@patternfly/context-for-ai 1.2.0
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/README.md +615 -0
- package/codemod/ALL_COMPONENTS_REFERENCE.md +815 -0
- package/codemod/ATTRIBUTE_DECISION_LOGIC.md +320 -0
- package/codemod/README.md +400 -0
- package/codemod/add-semantic-attributes.sh +69 -0
- package/codemod/component-attributes-reference.json +129 -0
- package/codemod/example-after.tsx +51 -0
- package/codemod/example-before.tsx +19 -0
- package/codemod/static-inference.js +5015 -0
- package/codemod/transform.js +1108 -0
- package/dist/components/advanced/index.d.ts +2 -0
- package/dist/components/advanced/index.d.ts.map +1 -0
- package/dist/components/core/Button.d.ts +14 -0
- package/dist/components/core/Button.d.ts.map +1 -0
- package/dist/components/core/Link.d.ts +15 -0
- package/dist/components/core/Link.d.ts.map +1 -0
- package/dist/components/core/StarIcon.d.ts +15 -0
- package/dist/components/core/StarIcon.d.ts.map +1 -0
- package/dist/components/core/index.d.ts +4 -0
- package/dist/components/core/index.d.ts.map +1 -0
- package/dist/components/data-display/Card.d.ts +14 -0
- package/dist/components/data-display/Card.d.ts.map +1 -0
- package/dist/components/data-display/StatusBadge.d.ts +13 -0
- package/dist/components/data-display/StatusBadge.d.ts.map +1 -0
- package/dist/components/data-display/Tbody.d.ts +12 -0
- package/dist/components/data-display/Tbody.d.ts.map +1 -0
- package/dist/components/data-display/Td.d.ts +14 -0
- package/dist/components/data-display/Td.d.ts.map +1 -0
- package/dist/components/data-display/Th.d.ts +14 -0
- package/dist/components/data-display/Th.d.ts.map +1 -0
- package/dist/components/data-display/Thead.d.ts +12 -0
- package/dist/components/data-display/Thead.d.ts.map +1 -0
- package/dist/components/data-display/Tr.d.ts +16 -0
- package/dist/components/data-display/Tr.d.ts.map +1 -0
- package/dist/components/data-display/index.d.ts +8 -0
- package/dist/components/data-display/index.d.ts.map +1 -0
- package/dist/components/feedback/index.d.ts +2 -0
- package/dist/components/feedback/index.d.ts.map +1 -0
- package/dist/components/forms/Checkbox.d.ts +16 -0
- package/dist/components/forms/Checkbox.d.ts.map +1 -0
- package/dist/components/forms/Form.d.ts +12 -0
- package/dist/components/forms/Form.d.ts.map +1 -0
- package/dist/components/forms/Radio.d.ts +32 -0
- package/dist/components/forms/Radio.d.ts.map +1 -0
- package/dist/components/forms/Select.d.ts +33 -0
- package/dist/components/forms/Select.d.ts.map +1 -0
- package/dist/components/forms/Switch.d.ts +31 -0
- package/dist/components/forms/Switch.d.ts.map +1 -0
- package/dist/components/forms/TextArea.d.ts +29 -0
- package/dist/components/forms/TextArea.d.ts.map +1 -0
- package/dist/components/forms/TextInput.d.ts +29 -0
- package/dist/components/forms/TextInput.d.ts.map +1 -0
- package/dist/components/forms/index.d.ts +8 -0
- package/dist/components/forms/index.d.ts.map +1 -0
- package/dist/components/index.d.ts +9 -0
- package/dist/components/index.d.ts.map +1 -0
- package/dist/components/layout/Flex.d.ts +16 -0
- package/dist/components/layout/Flex.d.ts.map +1 -0
- package/dist/components/layout/FlexItem.d.ts +16 -0
- package/dist/components/layout/FlexItem.d.ts.map +1 -0
- package/dist/components/layout/index.d.ts +3 -0
- package/dist/components/layout/index.d.ts.map +1 -0
- package/dist/components/navigation/DropdownItem.d.ts +8 -0
- package/dist/components/navigation/DropdownItem.d.ts.map +1 -0
- package/dist/components/navigation/MenuToggle.d.ts +8 -0
- package/dist/components/navigation/MenuToggle.d.ts.map +1 -0
- package/dist/components/navigation/index.d.ts +3 -0
- package/dist/components/navigation/index.d.ts.map +1 -0
- package/dist/components/overlay/Drawer.d.ts +12 -0
- package/dist/components/overlay/Drawer.d.ts.map +1 -0
- package/dist/components/overlay/Modal.d.ts +16 -0
- package/dist/components/overlay/Modal.d.ts.map +1 -0
- package/dist/components/overlay/index.d.ts +3 -0
- package/dist/components/overlay/index.d.ts.map +1 -0
- package/dist/context/SemanticContext.d.ts +28 -0
- package/dist/context/SemanticContext.d.ts.map +1 -0
- package/dist/hooks/index.d.ts +3 -0
- package/dist/hooks/index.d.ts.map +1 -0
- package/dist/hooks/useAccessibility.d.ts +13 -0
- package/dist/hooks/useAccessibility.d.ts.map +1 -0
- package/dist/hooks/useSemanticMetadata.d.ts +9 -0
- package/dist/hooks/useSemanticMetadata.d.ts.map +1 -0
- package/dist/index.d.ts +574 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.esm.js +1362 -0
- package/dist/index.esm.js.map +1 -0
- package/dist/index.js +1426 -0
- package/dist/index.js.map +1 -0
- package/dist/types/index.d.ts +47 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/utils/accessibility.d.ts +16 -0
- package/dist/utils/accessibility.d.ts.map +1 -0
- package/dist/utils/index.d.ts +4 -0
- package/dist/utils/index.d.ts.map +1 -0
- package/dist/utils/inference.d.ts +136 -0
- package/dist/utils/inference.d.ts.map +1 -0
- package/dist/utils/metadata.d.ts +17 -0
- package/dist/utils/metadata.d.ts.map +1 -0
- package/package.json +104 -0
|
@@ -0,0 +1,1108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* JSCodeShift Transform: Add Semantic Data Attributes to PatternFly Components
|
|
3
|
+
*
|
|
4
|
+
* This transform adds standardized data-* attributes to all PatternFly components
|
|
5
|
+
* in user code, making them more AI-friendly. Attributes appear on rendered DOM elements.
|
|
6
|
+
*
|
|
7
|
+
* Usage:
|
|
8
|
+
* jscodeshift -t codemod/transform.js path/to/files
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const {
|
|
12
|
+
inferRole,
|
|
13
|
+
inferPurpose,
|
|
14
|
+
inferVariant,
|
|
15
|
+
inferContext,
|
|
16
|
+
inferState,
|
|
17
|
+
inferActionType,
|
|
18
|
+
inferSize,
|
|
19
|
+
inferActionListVariant,
|
|
20
|
+
isPatternFlyComponent,
|
|
21
|
+
STANDARD_ATTRIBUTES,
|
|
22
|
+
} = require('./static-inference');
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Semantic attribute names (without 'data-' prefix)
|
|
26
|
+
*/
|
|
27
|
+
const SEMANTIC_ATTR_NAMES = STANDARD_ATTRIBUTES.map(attr => attr.replace('data-', ''));
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Check if element already has semantic attributes
|
|
31
|
+
*/
|
|
32
|
+
function hasSemanticAttributes(attributes) {
|
|
33
|
+
if (!attributes || attributes.length === 0) {
|
|
34
|
+
return false;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
return attributes.some(attr => {
|
|
38
|
+
const name = attr.name?.name || attr.name?.value;
|
|
39
|
+
if (!name) return false;
|
|
40
|
+
|
|
41
|
+
// Check for old format (data-semantic-*) or new format (data-role, etc.)
|
|
42
|
+
return name.startsWith('data-semantic-') || SEMANTIC_ATTR_NAMES.includes(name.replace('data-', ''));
|
|
43
|
+
});
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Find parent context by traversing up the AST
|
|
48
|
+
* Returns an object with { context, purpose } if parent is Form, or just context string for others
|
|
49
|
+
*/
|
|
50
|
+
function findParentContext(path, imports) {
|
|
51
|
+
let current = path.parent;
|
|
52
|
+
let depth = 0;
|
|
53
|
+
const maxDepth = 10; // Prevent infinite loops
|
|
54
|
+
|
|
55
|
+
while (current && depth < maxDepth) {
|
|
56
|
+
if (current.value) {
|
|
57
|
+
const node = current.value;
|
|
58
|
+
|
|
59
|
+
// Check if parent is a JSX element
|
|
60
|
+
if (node.type === 'JSXElement' && node.openingElement) {
|
|
61
|
+
const parentName = node.openingElement.name?.name;
|
|
62
|
+
if (parentName && isPatternFlyComponent(parentName, imports)) {
|
|
63
|
+
const parentProps = node.openingElement.attributes || [];
|
|
64
|
+
const purpose = inferPurpose(parentName, parentProps);
|
|
65
|
+
const context = inferContext(parentName, parentProps);
|
|
66
|
+
|
|
67
|
+
// For Page, return page context so children inherit it
|
|
68
|
+
if (parentName.toLowerCase().includes('page') && !parentName.toLowerCase().includes('pagesection') &&
|
|
69
|
+
!parentName.toLowerCase().includes('pageheader') && !parentName.toLowerCase().includes('pagebody') &&
|
|
70
|
+
!parentName.toLowerCase().includes('pagefooter')) {
|
|
71
|
+
return 'page';
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// For Sidebar, return sidebar context so children inherit it
|
|
75
|
+
if (parentName.toLowerCase().includes('sidebar') && !parentName.toLowerCase().includes('sidebarcontent') &&
|
|
76
|
+
!parentName.toLowerCase().includes('sidebarpanel')) {
|
|
77
|
+
return 'sidebar';
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
// For Toolbar, return toolbar context so children inherit it
|
|
81
|
+
if (parentName.toLowerCase().includes('toolbar') && !parentName.toLowerCase().includes('toolbaritem') &&
|
|
82
|
+
!parentName.toLowerCase().includes('toolbaritemgroup')) {
|
|
83
|
+
return 'toolbar';
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// For Wizard, return wizard context so children inherit it
|
|
87
|
+
if (parentName.toLowerCase().includes('wizard') && !parentName.toLowerCase().includes('wizardnav') &&
|
|
88
|
+
!parentName.toLowerCase().includes('wizardnavitem') && !parentName.toLowerCase().includes('wizardbody') &&
|
|
89
|
+
!parentName.toLowerCase().includes('wizardfooter')) {
|
|
90
|
+
return 'wizard';
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// For Form, return both context and purpose so children can inherit purpose
|
|
94
|
+
if (purpose === 'form-container') {
|
|
95
|
+
return { context, purpose };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// For LoginForm, return authentication context so children inherit it
|
|
99
|
+
if (purpose === 'authentication' || parentName.toLowerCase().includes('loginform')) {
|
|
100
|
+
return 'authentication';
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// For overlay components, just return context
|
|
104
|
+
if (purpose === 'overlay') {
|
|
105
|
+
return context;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
current = current.parent;
|
|
112
|
+
depth++;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return null;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Analyze ClipboardCopy children to infer content type variants (with-array, json-object)
|
|
120
|
+
* Traverses the AST to find array or JSON object content in children
|
|
121
|
+
*/
|
|
122
|
+
function analyzeClipboardCopyChildren(j, path) {
|
|
123
|
+
let hasArray = false;
|
|
124
|
+
let hasJsonObject = false;
|
|
125
|
+
|
|
126
|
+
// Find the JSXElement that contains this opening element
|
|
127
|
+
let parentElement = null;
|
|
128
|
+
let current = path.parent;
|
|
129
|
+
|
|
130
|
+
// Traverse up to find the JSXElement
|
|
131
|
+
while (current && !parentElement) {
|
|
132
|
+
if (current.value && current.value.type === 'JSXElement') {
|
|
133
|
+
parentElement = current.value;
|
|
134
|
+
break;
|
|
135
|
+
}
|
|
136
|
+
current = current.parent;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
if (!parentElement || !parentElement.children) {
|
|
140
|
+
return { hasArray: false, hasJsonObject: false };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Look through children for text content that might indicate array or JSON
|
|
144
|
+
// Check for JSXText or JSXExpressionContainer with array/object patterns
|
|
145
|
+
parentElement.children.forEach(child => {
|
|
146
|
+
if (child.type === 'JSXText') {
|
|
147
|
+
const text = child.value.trim();
|
|
148
|
+
// Check for JSON object pattern (starts with { or contains JSON-like structure)
|
|
149
|
+
if (text.startsWith('{') || (text.includes('"') && (text.includes(':') || text.includes(',')))) {
|
|
150
|
+
hasJsonObject = true;
|
|
151
|
+
}
|
|
152
|
+
// Check for array pattern (starts with [)
|
|
153
|
+
if (text.startsWith('[')) {
|
|
154
|
+
hasArray = true;
|
|
155
|
+
}
|
|
156
|
+
} else if (child.type === 'JSXExpressionContainer' && child.expression) {
|
|
157
|
+
// Check for ArrayExpression or ObjectExpression in JSX expressions
|
|
158
|
+
if (child.expression.type === 'ArrayExpression') {
|
|
159
|
+
hasArray = true;
|
|
160
|
+
} else if (child.expression.type === 'ObjectExpression') {
|
|
161
|
+
hasJsonObject = true;
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
return { hasArray, hasJsonObject };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Analyze InputGroup children to infer variant (with-button-left, with-button-right, with-buttons-both, with-icon-before, with-icon-after, with-text-prefix, with-text-suffix, search)
|
|
171
|
+
* Traverses the AST to find buttons, text, icons, or search icons in InputGroup
|
|
172
|
+
*/
|
|
173
|
+
function analyzeInputGroupChildren(j, path) {
|
|
174
|
+
let hasButtonLeft = false;
|
|
175
|
+
let hasButtonRight = false;
|
|
176
|
+
let hasIconLeft = false;
|
|
177
|
+
let hasIconRight = false;
|
|
178
|
+
let hasTextLeft = false;
|
|
179
|
+
let hasTextRight = false;
|
|
180
|
+
let hasSearch = false;
|
|
181
|
+
|
|
182
|
+
// Find the JSXElement that contains this opening element
|
|
183
|
+
let parentElement = null;
|
|
184
|
+
let current = path.parent;
|
|
185
|
+
|
|
186
|
+
// Traverse up to find the JSXElement
|
|
187
|
+
while (current && !parentElement) {
|
|
188
|
+
if (current.value && current.value.type === 'JSXElement') {
|
|
189
|
+
parentElement = current.value;
|
|
190
|
+
break;
|
|
191
|
+
}
|
|
192
|
+
current = current.parent;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (!parentElement || !parentElement.children) {
|
|
196
|
+
return { hasButtonLeft: false, hasButtonRight: false, hasIconLeft: false, hasIconRight: false, hasTextLeft: false, hasTextRight: false, hasSearch: false };
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// Look through children for buttons, text, or search icons
|
|
200
|
+
// InputGroup children are typically in order: [left element, input, right element]
|
|
201
|
+
let foundInput = false;
|
|
202
|
+
parentElement.children.forEach(child => {
|
|
203
|
+
if (child.type === 'JSXElement' && child.openingElement) {
|
|
204
|
+
const childName = child.openingElement.name?.name;
|
|
205
|
+
if (childName) {
|
|
206
|
+
const name = childName.toLowerCase();
|
|
207
|
+
// Check if this is the input (middle element)
|
|
208
|
+
if (name.includes('input') || name.includes('textinput') || name.includes('textarea') || name.includes('select')) {
|
|
209
|
+
foundInput = true;
|
|
210
|
+
return; // Skip input itself
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
// If we haven't found the input yet, this is a left element
|
|
214
|
+
// If we've found the input, this is a right element
|
|
215
|
+
const isLeft = !foundInput;
|
|
216
|
+
|
|
217
|
+
// Check for buttons
|
|
218
|
+
if (name.includes('button')) {
|
|
219
|
+
if (isLeft) {
|
|
220
|
+
hasButtonLeft = true;
|
|
221
|
+
} else {
|
|
222
|
+
hasButtonRight = true;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
// Check for icons (components ending with "icon" or "Icon")
|
|
226
|
+
if (name.endsWith('icon')) {
|
|
227
|
+
if (isLeft) {
|
|
228
|
+
hasIconLeft = true;
|
|
229
|
+
} else {
|
|
230
|
+
hasIconRight = true;
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
// Check for InputGroupText (text prefix/suffix, not icon)
|
|
234
|
+
if (name.includes('inputgrouptext')) {
|
|
235
|
+
if (isLeft) {
|
|
236
|
+
hasTextLeft = true;
|
|
237
|
+
} else {
|
|
238
|
+
hasTextRight = true;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
// Check for search icon/button
|
|
242
|
+
if (name.includes('search') || name.includes('searchicon')) {
|
|
243
|
+
hasSearch = true;
|
|
244
|
+
// Search button is typically on the right
|
|
245
|
+
if (!isLeft) {
|
|
246
|
+
hasButtonRight = true;
|
|
247
|
+
hasIconRight = true;
|
|
248
|
+
} else {
|
|
249
|
+
hasIconLeft = true;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
return { hasButtonLeft, hasButtonRight, hasIconLeft, hasIconRight, hasTextLeft, hasTextRight, hasSearch };
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Analyze List children to infer variant (with-icons, small-icons, big-icons)
|
|
261
|
+
* Traverses the AST to find icons in ListItem children
|
|
262
|
+
*/
|
|
263
|
+
function analyzeListChildren(j, path) {
|
|
264
|
+
let hasIcons = false;
|
|
265
|
+
let hasSmallIcons = false;
|
|
266
|
+
let hasBigIcons = false;
|
|
267
|
+
|
|
268
|
+
// Find the JSXElement that contains this opening element
|
|
269
|
+
let parentElement = null;
|
|
270
|
+
let current = path.parent;
|
|
271
|
+
|
|
272
|
+
// Traverse up to find the JSXElement
|
|
273
|
+
while (current && !parentElement) {
|
|
274
|
+
if (current.value && current.value.type === 'JSXElement') {
|
|
275
|
+
parentElement = current.value;
|
|
276
|
+
break;
|
|
277
|
+
}
|
|
278
|
+
current = current.parent;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
if (!parentElement || !parentElement.children) {
|
|
282
|
+
return { hasIcons: false, hasSmallIcons: false, hasBigIcons: false };
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// Look through children for ListItem components with icons
|
|
286
|
+
parentElement.children.forEach(child => {
|
|
287
|
+
if (child.type === 'JSXElement' && child.openingElement) {
|
|
288
|
+
const childName = child.openingElement.name?.name;
|
|
289
|
+
if (childName && childName.toLowerCase().includes('listitem')) {
|
|
290
|
+
// Check if ListItem has icon children
|
|
291
|
+
if (child.children) {
|
|
292
|
+
child.children.forEach(grandchild => {
|
|
293
|
+
if (grandchild.type === 'JSXElement' && grandchild.openingElement) {
|
|
294
|
+
const grandchildName = grandchild.openingElement.name?.name;
|
|
295
|
+
if (grandchildName) {
|
|
296
|
+
const name = grandchildName.toLowerCase();
|
|
297
|
+
// Check for icon components (ending with "icon" or "Icon")
|
|
298
|
+
if (name.endsWith('icon')) {
|
|
299
|
+
hasIcons = true;
|
|
300
|
+
// Check icon size if available
|
|
301
|
+
const iconProps = grandchild.openingElement.attributes || [];
|
|
302
|
+
const sizeAttr = iconProps.find(attr =>
|
|
303
|
+
attr.name?.name === 'size'
|
|
304
|
+
);
|
|
305
|
+
if (sizeAttr && sizeAttr.value) {
|
|
306
|
+
const size = sizeAttr.value.type === 'StringLiteral'
|
|
307
|
+
? sizeAttr.value.value.toLowerCase()
|
|
308
|
+
: null;
|
|
309
|
+
if (size === 'sm' || size === 'small') {
|
|
310
|
+
hasSmallIcons = true;
|
|
311
|
+
} else if (size === 'lg' || size === 'large' || size === 'xl') {
|
|
312
|
+
hasBigIcons = true;
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
}
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
return { hasIcons, hasSmallIcons, hasBigIcons };
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/**
|
|
328
|
+
* Analyze LoginForm children to infer variant (show-hide-password, customized-header-utilities)
|
|
329
|
+
* Traverses the AST to find password toggle or customized header utilities
|
|
330
|
+
*/
|
|
331
|
+
function analyzeLoginFormChildren(j, path) {
|
|
332
|
+
let hasShowHidePassword = false;
|
|
333
|
+
let hasCustomizedHeaderUtilities = false;
|
|
334
|
+
|
|
335
|
+
// Find the JSXElement that contains this opening element
|
|
336
|
+
let parentElement = null;
|
|
337
|
+
let current = path.parent;
|
|
338
|
+
|
|
339
|
+
// Traverse up to find the JSXElement
|
|
340
|
+
while (current && !parentElement) {
|
|
341
|
+
if (current.value && current.value.type === 'JSXElement') {
|
|
342
|
+
parentElement = current.value;
|
|
343
|
+
break;
|
|
344
|
+
}
|
|
345
|
+
current = current.parent;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (!parentElement || !parentElement.children) {
|
|
349
|
+
return { hasShowHidePassword: false, hasCustomizedHeaderUtilities: false };
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
// Look through children for password toggle or customized header utilities
|
|
353
|
+
parentElement.children.forEach(child => {
|
|
354
|
+
if (child.type === 'JSXElement' && child.openingElement) {
|
|
355
|
+
const childName = child.openingElement.name?.name;
|
|
356
|
+
if (childName) {
|
|
357
|
+
const name = childName.toLowerCase();
|
|
358
|
+
// Check for InputGroup containing password input with toggle button
|
|
359
|
+
if (name.includes('inputgroup')) {
|
|
360
|
+
// Check if InputGroup contains password input and button
|
|
361
|
+
let hasPasswordInput = false;
|
|
362
|
+
let hasToggleButton = false;
|
|
363
|
+
if (child.children) {
|
|
364
|
+
child.children.forEach(grandchild => {
|
|
365
|
+
if (grandchild.type === 'JSXElement' && grandchild.openingElement) {
|
|
366
|
+
const grandchildName = grandchild.openingElement.name?.name;
|
|
367
|
+
if (grandchildName) {
|
|
368
|
+
const gcName = grandchildName.toLowerCase();
|
|
369
|
+
// Check for password input
|
|
370
|
+
if (gcName.includes('textinput') || gcName.includes('input')) {
|
|
371
|
+
const inputProps = grandchild.openingElement.attributes || [];
|
|
372
|
+
const typeAttr = inputProps.find(attr => attr.name?.name === 'type');
|
|
373
|
+
if (typeAttr && typeAttr.value) {
|
|
374
|
+
const typeValue = typeAttr.value.type === 'StringLiteral'
|
|
375
|
+
? typeAttr.value.value.toLowerCase()
|
|
376
|
+
: '';
|
|
377
|
+
if (typeValue === 'password') {
|
|
378
|
+
hasPasswordInput = true;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
// Check for button (toggle)
|
|
383
|
+
if (gcName.includes('button')) {
|
|
384
|
+
hasToggleButton = true;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
});
|
|
389
|
+
}
|
|
390
|
+
if (hasPasswordInput && hasToggleButton) {
|
|
391
|
+
hasShowHidePassword = true;
|
|
392
|
+
}
|
|
393
|
+
}
|
|
394
|
+
// Check for password input with show/hide toggle prop
|
|
395
|
+
if (name.includes('textinput') || name.includes('input')) {
|
|
396
|
+
const inputProps = child.openingElement.attributes || [];
|
|
397
|
+
const typeAttr = inputProps.find(attr => attr.name?.name === 'type');
|
|
398
|
+
const hasToggle = inputProps.some(attr =>
|
|
399
|
+
attr.name?.name === 'showPasswordToggle' ||
|
|
400
|
+
attr.name?.name === 'isPasswordToggleVisible' ||
|
|
401
|
+
attr.name?.name === 'onTogglePassword'
|
|
402
|
+
);
|
|
403
|
+
if (typeAttr && typeAttr.value) {
|
|
404
|
+
const typeValue = typeAttr.value.type === 'StringLiteral'
|
|
405
|
+
? typeAttr.value.value.toLowerCase()
|
|
406
|
+
: '';
|
|
407
|
+
if (typeValue === 'password' && hasToggle) {
|
|
408
|
+
hasShowHidePassword = true;
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
// Check for customized header utilities (LoginHeader, LoginHeaderUtilities, etc.)
|
|
413
|
+
if (name.includes('loginheader') || name.includes('header') ||
|
|
414
|
+
(name.includes('utilities') && name.includes('login')) ||
|
|
415
|
+
(name.includes('brand') && name.includes('login')) ||
|
|
416
|
+
(name.includes('logo') && name.includes('login'))) {
|
|
417
|
+
hasCustomizedHeaderUtilities = true;
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
});
|
|
422
|
+
|
|
423
|
+
return { hasShowHidePassword, hasCustomizedHeaderUtilities };
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Analyze Breadcrumb children to infer variant (with-dropdown, with-heading)
|
|
428
|
+
* Traverses the AST to find BreadcrumbDropdown or BreadcrumbHeading children
|
|
429
|
+
*/
|
|
430
|
+
function analyzeBreadcrumbChildren(j, path) {
|
|
431
|
+
let hasDropdown = false;
|
|
432
|
+
let hasHeading = false;
|
|
433
|
+
|
|
434
|
+
// Find the JSXElement that contains this opening element
|
|
435
|
+
let parentElement = null;
|
|
436
|
+
let current = path.parent;
|
|
437
|
+
|
|
438
|
+
// Traverse up to find the JSXElement
|
|
439
|
+
while (current && !parentElement) {
|
|
440
|
+
if (current.value && current.value.type === 'JSXElement') {
|
|
441
|
+
parentElement = current.value;
|
|
442
|
+
break;
|
|
443
|
+
}
|
|
444
|
+
current = current.parent;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
if (!parentElement || !parentElement.children) {
|
|
448
|
+
return { hasDropdown: false, hasHeading: false };
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
// Look through children for BreadcrumbDropdown or BreadcrumbHeading
|
|
452
|
+
parentElement.children.forEach(child => {
|
|
453
|
+
if (child.type === 'JSXElement' && child.openingElement) {
|
|
454
|
+
const childName = child.openingElement.name?.name;
|
|
455
|
+
if (childName) {
|
|
456
|
+
const name = childName.toLowerCase();
|
|
457
|
+
if (name.includes('breadcrumbdropdown') || name.includes('dropdown')) {
|
|
458
|
+
hasDropdown = true;
|
|
459
|
+
}
|
|
460
|
+
if (name.includes('breadcrumbheading') || name.includes('heading')) {
|
|
461
|
+
hasHeading = true;
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
return { hasDropdown, hasHeading };
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/**
|
|
471
|
+
* Analyze ActionList children to infer grouping variant
|
|
472
|
+
* Traverses the AST to find ActionListItem children and their button/kebab contents
|
|
473
|
+
*/
|
|
474
|
+
function analyzeActionListChildren(j, path) {
|
|
475
|
+
const children = [];
|
|
476
|
+
|
|
477
|
+
// Find the JSXElement that contains this opening element
|
|
478
|
+
// path.node is the JSXOpeningElement, we need the parent JSXElement
|
|
479
|
+
let parentElement = null;
|
|
480
|
+
let current = path.parent;
|
|
481
|
+
|
|
482
|
+
// Traverse up to find the JSXElement
|
|
483
|
+
while (current && !parentElement) {
|
|
484
|
+
if (current.value && current.value.type === 'JSXElement') {
|
|
485
|
+
parentElement = current.value;
|
|
486
|
+
break;
|
|
487
|
+
}
|
|
488
|
+
current = current.parent;
|
|
489
|
+
}
|
|
490
|
+
|
|
491
|
+
if (!parentElement || !parentElement.children) {
|
|
492
|
+
return children;
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
// Look through children for ActionListItem components
|
|
496
|
+
parentElement.children.forEach(child => {
|
|
497
|
+
if (child.type === 'JSXElement' && child.openingElement) {
|
|
498
|
+
const childName = child.openingElement.name?.name;
|
|
499
|
+
if (childName && childName.toLowerCase().includes('actionlistitem')) {
|
|
500
|
+
// Look for buttons or kebab inside ActionListItem
|
|
501
|
+
if (child.children) {
|
|
502
|
+
child.children.forEach(grandchild => {
|
|
503
|
+
if (grandchild.type === 'JSXElement' && grandchild.openingElement) {
|
|
504
|
+
const grandchildName = grandchild.openingElement.name?.name;
|
|
505
|
+
if (grandchildName) {
|
|
506
|
+
const name = grandchildName.toLowerCase();
|
|
507
|
+
if (name.includes('button')) {
|
|
508
|
+
// Check button variant
|
|
509
|
+
const buttonProps = grandchild.openingElement.attributes || [];
|
|
510
|
+
const variantAttr = buttonProps.find(attr =>
|
|
511
|
+
attr.name?.name === 'variant'
|
|
512
|
+
);
|
|
513
|
+
if (variantAttr && variantAttr.value) {
|
|
514
|
+
const variant = variantAttr.value.type === 'StringLiteral'
|
|
515
|
+
? variantAttr.value.value
|
|
516
|
+
: 'button';
|
|
517
|
+
children.push(variant);
|
|
518
|
+
} else {
|
|
519
|
+
// No variant specified, default to 'button' (will be inferred as primary by button logic)
|
|
520
|
+
children.push('button');
|
|
521
|
+
}
|
|
522
|
+
} else if (name.includes('kebab') || name.includes('menutoggle') || name.includes('icon')) {
|
|
523
|
+
// Generic "icon" instead of "kebab" to cover kebab and other icons
|
|
524
|
+
children.push('icon');
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
});
|
|
533
|
+
|
|
534
|
+
return children;
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
/**
|
|
538
|
+
* Check if a form input has HelperText associated with it
|
|
539
|
+
* HelperText can be:
|
|
540
|
+
* 1. A parent wrapper: <HelperText><TextInput /></HelperText>
|
|
541
|
+
* 2. A sibling in the same parent: <FormGroup><TextInput /><HelperText /></FormGroup>
|
|
542
|
+
*
|
|
543
|
+
* HelperText is state-dependent (error, warning, success, indeterminate) and should be a variant of the input
|
|
544
|
+
*/
|
|
545
|
+
function checkForHelperText(j, path) {
|
|
546
|
+
let hasHelperText = false;
|
|
547
|
+
let helperTextState = null; // 'error', 'warning', 'success', 'indeterminate' (default)
|
|
548
|
+
|
|
549
|
+
// Helper function to extract state from HelperText props
|
|
550
|
+
function extractHelperTextState(helperTextProps) {
|
|
551
|
+
const propsMap = new Map();
|
|
552
|
+
helperTextProps.forEach(attr => {
|
|
553
|
+
if (attr.name && attr.name.name) {
|
|
554
|
+
let value = true; // Boolean shorthand
|
|
555
|
+
if (attr.value) {
|
|
556
|
+
if (attr.value.type === 'StringLiteral' || attr.value.type === 'Literal') {
|
|
557
|
+
value = attr.value.value;
|
|
558
|
+
} else if (attr.value.type === 'JSXExpressionContainer' && attr.value.expression) {
|
|
559
|
+
if (attr.value.expression.type === 'BooleanLiteral') {
|
|
560
|
+
value = attr.value.expression.value;
|
|
561
|
+
}
|
|
562
|
+
} else if (attr.value.type === 'BooleanLiteral') {
|
|
563
|
+
value = attr.value.value;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
propsMap.set(attr.name.name, value);
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
|
|
570
|
+
// Determine state from HelperText props (priority: error > warning > success > indeterminate)
|
|
571
|
+
if (propsMap.has('isError') && propsMap.get('isError')) {
|
|
572
|
+
return 'error';
|
|
573
|
+
} else if (propsMap.has('isWarning') && propsMap.get('isWarning')) {
|
|
574
|
+
return 'warning';
|
|
575
|
+
} else if (propsMap.has('isSuccess') && propsMap.get('isSuccess')) {
|
|
576
|
+
return 'success';
|
|
577
|
+
} else if (propsMap.has('isIndeterminate') && propsMap.get('isIndeterminate')) {
|
|
578
|
+
return 'indeterminate';
|
|
579
|
+
} else {
|
|
580
|
+
// Default state if no explicit state prop
|
|
581
|
+
return 'indeterminate';
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
// CASE 1: Check if HelperText is a parent wrapper (traverse UP)
|
|
586
|
+
let current = path.parent;
|
|
587
|
+
let depth = 0;
|
|
588
|
+
const maxDepth = 10; // Prevent infinite loops
|
|
589
|
+
|
|
590
|
+
while (current && depth < maxDepth) {
|
|
591
|
+
if (current.value) {
|
|
592
|
+
const node = current.value;
|
|
593
|
+
|
|
594
|
+
// Check if parent is a JSX element
|
|
595
|
+
if (node.type === 'JSXElement' && node.openingElement) {
|
|
596
|
+
const parentName = node.openingElement.name?.name;
|
|
597
|
+
if (parentName) {
|
|
598
|
+
const name = parentName.toLowerCase();
|
|
599
|
+
// Check if this parent is HelperText
|
|
600
|
+
if (name.includes('helpertext') || name.includes('helper-text')) {
|
|
601
|
+
hasHelperText = true;
|
|
602
|
+
const helperTextProps = node.openingElement.attributes || [];
|
|
603
|
+
helperTextState = extractHelperTextState(helperTextProps);
|
|
604
|
+
// Found HelperText parent, return early
|
|
605
|
+
return { hasHelperText, helperTextState };
|
|
606
|
+
}
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
|
|
611
|
+
current = current.parent;
|
|
612
|
+
depth++;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// CASE 2: Check if HelperText is a sibling in the same parent container
|
|
616
|
+
// Find the parent JSXElement that contains this component
|
|
617
|
+
let parentElement = null;
|
|
618
|
+
current = path.parent;
|
|
619
|
+
depth = 0;
|
|
620
|
+
|
|
621
|
+
while (current && depth < maxDepth && !parentElement) {
|
|
622
|
+
if (current.value && current.value.type === 'JSXElement') {
|
|
623
|
+
parentElement = current.value;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
current = current.parent;
|
|
627
|
+
depth++;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if (parentElement && parentElement.children) {
|
|
631
|
+
// Look through siblings for HelperText component
|
|
632
|
+
parentElement.children.forEach(child => {
|
|
633
|
+
if (child.type === 'JSXElement' && child.openingElement) {
|
|
634
|
+
const childName = child.openingElement.name?.name;
|
|
635
|
+
if (childName) {
|
|
636
|
+
const name = childName.toLowerCase();
|
|
637
|
+
// Check for HelperText component (sibling)
|
|
638
|
+
if (name.includes('helpertext') || name.includes('helper-text')) {
|
|
639
|
+
hasHelperText = true;
|
|
640
|
+
const helperTextProps = child.openingElement.attributes || [];
|
|
641
|
+
helperTextState = extractHelperTextState(helperTextProps);
|
|
642
|
+
// Found HelperText sibling, stop looking
|
|
643
|
+
return;
|
|
644
|
+
}
|
|
645
|
+
}
|
|
646
|
+
}
|
|
647
|
+
});
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
return { hasHelperText, helperTextState };
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
/**
|
|
654
|
+
* Analyze DualListSelector children to infer variants (with-tooltips, with-search, with-actions, multiple-drop-zones)
|
|
655
|
+
* Traverses the AST to find search inputs, action menus, tooltips, and multiple drop zones
|
|
656
|
+
*/
|
|
657
|
+
function analyzeDualListSelectorChildren(j, path) {
|
|
658
|
+
let hasTooltips = false;
|
|
659
|
+
let hasSearch = false;
|
|
660
|
+
let hasActions = false;
|
|
661
|
+
let hasMultipleDropZones = false;
|
|
662
|
+
|
|
663
|
+
// Find the JSXElement that contains this opening element
|
|
664
|
+
let parentElement = null;
|
|
665
|
+
let current = path.parent;
|
|
666
|
+
|
|
667
|
+
// Traverse up to find the JSXElement
|
|
668
|
+
while (current && !parentElement) {
|
|
669
|
+
if (current.value && current.value.type === 'JSXElement') {
|
|
670
|
+
parentElement = current.value;
|
|
671
|
+
break;
|
|
672
|
+
}
|
|
673
|
+
current = current.parent;
|
|
674
|
+
}
|
|
675
|
+
|
|
676
|
+
if (!parentElement || !parentElement.children) {
|
|
677
|
+
return { hasTooltips: false, hasSearch: false, hasActions: false, hasMultipleDropZones: false };
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
let dropZoneCount = 0;
|
|
681
|
+
|
|
682
|
+
// Look through children for search, actions, tooltips, and drop zones
|
|
683
|
+
parentElement.children.forEach(child => {
|
|
684
|
+
if (child.type === 'JSXElement' && child.openingElement) {
|
|
685
|
+
const childName = child.openingElement.name?.name;
|
|
686
|
+
if (childName) {
|
|
687
|
+
const name = childName.toLowerCase();
|
|
688
|
+
// Check for search/filter components
|
|
689
|
+
if (name.includes('search') || name.includes('filter') || name.includes('input')) {
|
|
690
|
+
hasSearch = true;
|
|
691
|
+
}
|
|
692
|
+
// Check for action menus (kebab, menu, actions)
|
|
693
|
+
if (name.includes('kebab') || name.includes('menutoggle') || name.includes('actionmenu') ||
|
|
694
|
+
name.includes('actions')) {
|
|
695
|
+
hasActions = true;
|
|
696
|
+
}
|
|
697
|
+
// Check for tooltips
|
|
698
|
+
if (name.includes('tooltip')) {
|
|
699
|
+
hasTooltips = true;
|
|
700
|
+
}
|
|
701
|
+
// Check for drop zones (Droppable components)
|
|
702
|
+
if (name.includes('droppable')) {
|
|
703
|
+
dropZoneCount++;
|
|
704
|
+
}
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
});
|
|
708
|
+
|
|
709
|
+
hasMultipleDropZones = dropZoneCount > 1;
|
|
710
|
+
|
|
711
|
+
return { hasTooltips, hasSearch, hasActions, hasMultipleDropZones };
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
/**
|
|
715
|
+
* Create semantic data attributes
|
|
716
|
+
* Only adds attributes when we can infer meaningful values (not null)
|
|
717
|
+
* parentContext can be a string (context) or an object { context, purpose } for Form parents
|
|
718
|
+
*/
|
|
719
|
+
function createSemanticAttributes(j, componentName, props, parentContext, path = null) {
|
|
720
|
+
const role = inferRole(componentName);
|
|
721
|
+
|
|
722
|
+
// Extract parent context and purpose if parentContext is an object (Form case)
|
|
723
|
+
// Also used for purpose inference (ToggleGroup uses toolbar context for filtering)
|
|
724
|
+
let parentContextValue = null;
|
|
725
|
+
let parentPurpose = null;
|
|
726
|
+
if (typeof parentContext === 'object' && parentContext !== null) {
|
|
727
|
+
parentContextValue = parentContext.context;
|
|
728
|
+
parentPurpose = parentContext.purpose;
|
|
729
|
+
} else {
|
|
730
|
+
parentContextValue = parentContext;
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
const purpose = inferPurpose(componentName, props, parentContextValue);
|
|
734
|
+
|
|
735
|
+
// For ActionList, analyze children to infer grouping variant
|
|
736
|
+
// For Breadcrumb, analyze children to detect dropdown/heading variants
|
|
737
|
+
// For ClipboardCopy, analyze children to detect content type variants (array, json-object)
|
|
738
|
+
// For DualListSelector, analyze children to detect sub-variants (with-tooltips, with-search, with-actions, multiple-drop-zones)
|
|
739
|
+
// For form inputs (TextInput, TextArea, Select, etc.), check for HelperText as parent wrapper or sibling
|
|
740
|
+
// For InlineEdit, detect if it's in a table row context (row-editing variant)
|
|
741
|
+
// For InputGroup, analyze children to detect variant (with-button-left, with-button-right, with-buttons-both, with-icon-before, with-icon-after, with-text-prefix, with-text-suffix, search)
|
|
742
|
+
// For List, analyze children to detect icon variants (with-icons, small-icons, big-icons)
|
|
743
|
+
// For LoginForm, analyze children to detect variant (show-hide-password, customized-header-utilities)
|
|
744
|
+
let variant;
|
|
745
|
+
if (componentName.toLowerCase().includes('actionlist') && path) {
|
|
746
|
+
const children = analyzeActionListChildren(j, path);
|
|
747
|
+
variant = inferActionListVariant(children);
|
|
748
|
+
} else if (componentName.toLowerCase().includes('breadcrumb') &&
|
|
749
|
+
!componentName.toLowerCase().includes('breadcrumbitem') &&
|
|
750
|
+
!componentName.toLowerCase().includes('breadcrumbheading') &&
|
|
751
|
+
path) {
|
|
752
|
+
const childrenInfo = analyzeBreadcrumbChildren(j, path);
|
|
753
|
+
// Override variant if children analysis detects dropdown or heading
|
|
754
|
+
const baseVariant = inferVariant(componentName, props);
|
|
755
|
+
if (childrenInfo.hasDropdown) {
|
|
756
|
+
variant = 'with-dropdown';
|
|
757
|
+
} else if (childrenInfo.hasHeading) {
|
|
758
|
+
variant = 'with-heading';
|
|
759
|
+
} else {
|
|
760
|
+
variant = baseVariant;
|
|
761
|
+
}
|
|
762
|
+
} else if ((componentName.toLowerCase().includes('textinput') ||
|
|
763
|
+
componentName.toLowerCase().includes('textarea') ||
|
|
764
|
+
componentName.toLowerCase().includes('select') ||
|
|
765
|
+
componentName.toLowerCase().includes('checkbox') ||
|
|
766
|
+
componentName.toLowerCase().includes('radio') ||
|
|
767
|
+
componentName.toLowerCase().includes('switch')) &&
|
|
768
|
+
path) {
|
|
769
|
+
// Form inputs with HelperText (parent wrapper or sibling) get "with-helper-text" variant
|
|
770
|
+
const helperTextInfo = checkForHelperText(j, path);
|
|
771
|
+
const baseVariant = inferVariant(componentName, props);
|
|
772
|
+
if (helperTextInfo.hasHelperText) {
|
|
773
|
+
const variantParts = baseVariant ? baseVariant.split('-') : [];
|
|
774
|
+
if (!variantParts.includes('with-helper-text')) {
|
|
775
|
+
variantParts.push('with-helper-text');
|
|
776
|
+
}
|
|
777
|
+
variant = variantParts.length > 0 ? variantParts.join('-') : 'with-helper-text';
|
|
778
|
+
} else {
|
|
779
|
+
variant = baseVariant;
|
|
780
|
+
}
|
|
781
|
+
} else if (componentName.toLowerCase().includes('inlineedit') &&
|
|
782
|
+
!componentName.toLowerCase().includes('inlineedittoggle') &&
|
|
783
|
+
!componentName.toLowerCase().includes('inlineeditactiongroup') &&
|
|
784
|
+
!componentName.toLowerCase().includes('inlineeditinput') &&
|
|
785
|
+
path) {
|
|
786
|
+
// Detect if InlineEdit is in a table row context (row-editing variant)
|
|
787
|
+
// Traverse up the AST to find if parent is a table row (Tr)
|
|
788
|
+
let isInTableRow = false;
|
|
789
|
+
let current = path.parent;
|
|
790
|
+
let depth = 0;
|
|
791
|
+
const maxDepth = 10;
|
|
792
|
+
|
|
793
|
+
while (current && depth < maxDepth) {
|
|
794
|
+
if (current.value) {
|
|
795
|
+
const node = current.value;
|
|
796
|
+
if (node.type === 'JSXElement' && node.openingElement) {
|
|
797
|
+
const parentName = node.openingElement.name?.name;
|
|
798
|
+
if (parentName) {
|
|
799
|
+
const parentNameLower = parentName.toLowerCase();
|
|
800
|
+
// Check if parent is a table row (Tr)
|
|
801
|
+
if (parentNameLower === 'tr' || parentNameLower.includes('tablerow')) {
|
|
802
|
+
isInTableRow = true;
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
// If we hit a table or tbody, we're in table context but not necessarily a row
|
|
806
|
+
// Continue searching for Tr
|
|
807
|
+
if (parentNameLower.includes('table') || parentNameLower.includes('tbody') ||
|
|
808
|
+
parentNameLower.includes('thead')) {
|
|
809
|
+
// Continue searching
|
|
810
|
+
} else if (!parentNameLower.includes('td') && !parentNameLower.includes('th')) {
|
|
811
|
+
// If we hit something that's not a table-related component, stop searching
|
|
812
|
+
break;
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
}
|
|
817
|
+
current = current.parent;
|
|
818
|
+
depth++;
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
const baseVariant = inferVariant(componentName, props);
|
|
822
|
+
// If in table row context and no explicit variant, set to row-editing
|
|
823
|
+
if (isInTableRow && !baseVariant) {
|
|
824
|
+
variant = 'row-editing';
|
|
825
|
+
} else {
|
|
826
|
+
variant = baseVariant;
|
|
827
|
+
}
|
|
828
|
+
} else if (componentName.toLowerCase().includes('inputgroup') && path) {
|
|
829
|
+
const childrenInfo = analyzeInputGroupChildren(j, path);
|
|
830
|
+
const baseVariant = inferVariant(componentName, props);
|
|
831
|
+
|
|
832
|
+
// Determine variant based on children analysis
|
|
833
|
+
if (childrenInfo.hasSearch) {
|
|
834
|
+
variant = 'search';
|
|
835
|
+
} else if (childrenInfo.hasButtonLeft && childrenInfo.hasButtonRight) {
|
|
836
|
+
variant = 'with-buttons-both';
|
|
837
|
+
} else if (childrenInfo.hasButtonLeft) {
|
|
838
|
+
variant = 'with-button-left';
|
|
839
|
+
} else if (childrenInfo.hasButtonRight) {
|
|
840
|
+
variant = 'with-button-right';
|
|
841
|
+
} else if (childrenInfo.hasIconLeft) {
|
|
842
|
+
variant = 'with-icon-before';
|
|
843
|
+
} else if (childrenInfo.hasIconRight) {
|
|
844
|
+
variant = 'with-icon-after';
|
|
845
|
+
} else if (childrenInfo.hasTextLeft) {
|
|
846
|
+
variant = 'with-text-prefix';
|
|
847
|
+
} else if (childrenInfo.hasTextRight) {
|
|
848
|
+
variant = 'with-text-suffix';
|
|
849
|
+
} else {
|
|
850
|
+
variant = baseVariant;
|
|
851
|
+
}
|
|
852
|
+
} else if (componentName.toLowerCase().includes('list') &&
|
|
853
|
+
!componentName.toLowerCase().includes('listitem') &&
|
|
854
|
+
!componentName.toLowerCase().includes('datalist') &&
|
|
855
|
+
!componentName.toLowerCase().includes('actionlist') &&
|
|
856
|
+
!componentName.toLowerCase().includes('duallistselector') &&
|
|
857
|
+
path) {
|
|
858
|
+
const childrenInfo = analyzeListChildren(j, path);
|
|
859
|
+
const baseVariant = inferVariant(componentName, props);
|
|
860
|
+
|
|
861
|
+
// Add icon variants if detected
|
|
862
|
+
const variantParts = baseVariant ? baseVariant.split('-') : [];
|
|
863
|
+
if (childrenInfo.hasIcons) {
|
|
864
|
+
if (childrenInfo.hasBigIcons && !variantParts.includes('big-icons')) {
|
|
865
|
+
variantParts.push('big-icons');
|
|
866
|
+
} else if (childrenInfo.hasSmallIcons && !variantParts.includes('small-icons')) {
|
|
867
|
+
variantParts.push('small-icons');
|
|
868
|
+
} else if (!variantParts.includes('with-icons')) {
|
|
869
|
+
variantParts.push('with-icons');
|
|
870
|
+
}
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
variant = variantParts.length > 0 ? variantParts.join('-') : baseVariant;
|
|
874
|
+
} else if (componentName.toLowerCase().includes('loginform') && path) {
|
|
875
|
+
const childrenInfo = analyzeLoginFormChildren(j, path);
|
|
876
|
+
const baseVariant = inferVariant(componentName, props);
|
|
877
|
+
|
|
878
|
+
// Determine variant based on children analysis
|
|
879
|
+
if (childrenInfo.hasCustomizedHeaderUtilities) {
|
|
880
|
+
variant = 'customized-header-utilities';
|
|
881
|
+
} else if (childrenInfo.hasShowHidePassword) {
|
|
882
|
+
variant = 'show-hide-password';
|
|
883
|
+
} else if (baseVariant) {
|
|
884
|
+
variant = baseVariant;
|
|
885
|
+
} else {
|
|
886
|
+
// Default to basic
|
|
887
|
+
variant = 'basic';
|
|
888
|
+
}
|
|
889
|
+
} else if (componentName.toLowerCase().includes('clipboardcopy') && path) {
|
|
890
|
+
const childrenInfo = analyzeClipboardCopyChildren(j, path);
|
|
891
|
+
const baseVariant = inferVariant(componentName, props);
|
|
892
|
+
// Add content type variants if detected via children analysis
|
|
893
|
+
const variantParts = baseVariant ? baseVariant.split('-') : [];
|
|
894
|
+
if (childrenInfo.hasArray) {
|
|
895
|
+
// Only add if expandable (expanded with array)
|
|
896
|
+
if (variantParts.includes('expandable')) {
|
|
897
|
+
variantParts.push('with-array');
|
|
898
|
+
}
|
|
899
|
+
}
|
|
900
|
+
if (childrenInfo.hasJsonObject && !variantParts.includes('json-object')) {
|
|
901
|
+
variantParts.push('json-object');
|
|
902
|
+
}
|
|
903
|
+
variant = variantParts.length > 0 ? variantParts.join('-') : baseVariant;
|
|
904
|
+
} else if (componentName.toLowerCase().includes('duallistselector') &&
|
|
905
|
+
!componentName.toLowerCase().includes('duallistselectorpane') &&
|
|
906
|
+
!componentName.toLowerCase().includes('duallistselectorlist') &&
|
|
907
|
+
!componentName.toLowerCase().includes('duallistselectorlistitem') &&
|
|
908
|
+
path) {
|
|
909
|
+
const childrenInfo = analyzeDualListSelectorChildren(j, path);
|
|
910
|
+
const baseVariant = inferVariant(componentName, props);
|
|
911
|
+
// Add sub-variants if detected via children analysis
|
|
912
|
+
const variantParts = baseVariant ? baseVariant.split('-') : ['basic'];
|
|
913
|
+
if (childrenInfo.hasTooltips && !variantParts.includes('with-tooltips')) {
|
|
914
|
+
variantParts.push('with-tooltips');
|
|
915
|
+
}
|
|
916
|
+
if (childrenInfo.hasSearch && !variantParts.includes('with-search')) {
|
|
917
|
+
variantParts.push('with-search');
|
|
918
|
+
}
|
|
919
|
+
if (childrenInfo.hasActions && !variantParts.includes('with-actions')) {
|
|
920
|
+
variantParts.push('with-actions');
|
|
921
|
+
}
|
|
922
|
+
if (childrenInfo.hasMultipleDropZones && variantParts.includes('draggable') &&
|
|
923
|
+
!variantParts.includes('multiple-drop-zones')) {
|
|
924
|
+
variantParts.push('multiple-drop-zones');
|
|
925
|
+
}
|
|
926
|
+
variant = variantParts.join('-');
|
|
927
|
+
} else {
|
|
928
|
+
variant = inferVariant(componentName, props);
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
const context = inferContext(componentName, props, parentContextValue, parentPurpose);
|
|
932
|
+
|
|
933
|
+
// For form inputs, check if HelperText state should be incorporated into the input's state
|
|
934
|
+
let state = inferState(componentName, props);
|
|
935
|
+
if ((componentName.toLowerCase().includes('textinput') ||
|
|
936
|
+
componentName.toLowerCase().includes('textarea') ||
|
|
937
|
+
componentName.toLowerCase().includes('select') ||
|
|
938
|
+
componentName.toLowerCase().includes('checkbox') ||
|
|
939
|
+
componentName.toLowerCase().includes('radio') ||
|
|
940
|
+
componentName.toLowerCase().includes('switch')) &&
|
|
941
|
+
path) {
|
|
942
|
+
const helperTextInfo = checkForHelperText(j, path);
|
|
943
|
+
// HelperText state (error, warning, success) should be reflected in the input's state
|
|
944
|
+
if (helperTextInfo.hasHelperText && helperTextInfo.helperTextState) {
|
|
945
|
+
// Only add meaningful states (not indeterminate)
|
|
946
|
+
if (helperTextInfo.helperTextState !== 'indeterminate') {
|
|
947
|
+
// Combine states: if input already has a state, combine them; otherwise use helper text state
|
|
948
|
+
if (state) {
|
|
949
|
+
// Combine existing state with helper text state (e.g., "disabled error")
|
|
950
|
+
state = `${state} ${helperTextInfo.helperTextState}`;
|
|
951
|
+
} else {
|
|
952
|
+
state = helperTextInfo.helperTextState;
|
|
953
|
+
}
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
const actionType = inferActionType(componentName, props);
|
|
959
|
+
const size = inferSize(componentName, props);
|
|
960
|
+
|
|
961
|
+
const attributes = [];
|
|
962
|
+
|
|
963
|
+
// Always add purpose (it always has a value)
|
|
964
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier('data-purpose'), j.literal(purpose)));
|
|
965
|
+
|
|
966
|
+
// Only add role if we inferred a meaningful value (some structural children don't get roles)
|
|
967
|
+
if (role !== null) {
|
|
968
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier('data-role'), j.literal(role)));
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
// Only add variant, context, state, action-type, and size if we inferred meaningful values
|
|
972
|
+
if (variant !== null) {
|
|
973
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier('data-variant'), j.literal(variant)));
|
|
974
|
+
}
|
|
975
|
+
|
|
976
|
+
if (context !== null) {
|
|
977
|
+
// Render "in-page" instead of "page" for better clarity in HTML
|
|
978
|
+
const contextValue = context === 'page' ? 'in-page' : context;
|
|
979
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier('data-context'), j.literal(contextValue)));
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
if (state !== null) {
|
|
983
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier('data-state'), j.literal(state)));
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
if (actionType !== null) {
|
|
987
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier('data-action-type'), j.literal(actionType)));
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
if (size !== null) {
|
|
991
|
+
attributes.push(j.jsxAttribute(j.jsxIdentifier('data-size'), j.literal(size)));
|
|
992
|
+
}
|
|
993
|
+
|
|
994
|
+
return attributes;
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/**
|
|
998
|
+
* Main transform function
|
|
999
|
+
*/
|
|
1000
|
+
module.exports = function transformer(fileInfo, api) {
|
|
1001
|
+
const j = api.jscodeshift;
|
|
1002
|
+
const root = j(fileInfo.source);
|
|
1003
|
+
|
|
1004
|
+
// Track imports to identify PatternFly components
|
|
1005
|
+
const imports = [];
|
|
1006
|
+
root.find(j.ImportDeclaration).forEach(path => {
|
|
1007
|
+
imports.push(path.node);
|
|
1008
|
+
});
|
|
1009
|
+
|
|
1010
|
+
// Process all JSX opening elements (handles both self-closing and regular elements)
|
|
1011
|
+
root.find(j.JSXOpeningElement).forEach(path => {
|
|
1012
|
+
const node = path.node;
|
|
1013
|
+
const componentName = node.name?.name;
|
|
1014
|
+
|
|
1015
|
+
// Skip if not a valid component name
|
|
1016
|
+
if (!componentName || typeof componentName !== 'string') {
|
|
1017
|
+
return;
|
|
1018
|
+
}
|
|
1019
|
+
|
|
1020
|
+
// Check if this is a PatternFly component
|
|
1021
|
+
if (!isPatternFlyComponent(componentName, imports)) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
|
|
1025
|
+
// Skip structural child components - they don't need separate semantic attributes
|
|
1026
|
+
// The parent component has the attributes that describe the whole structure
|
|
1027
|
+
// This applies to components that are always used as children of a parent component
|
|
1028
|
+
const name = componentName.toLowerCase();
|
|
1029
|
+
|
|
1030
|
+
// Structural children to skip:
|
|
1031
|
+
// - Breadcrumb: BreadcrumbItem, BreadcrumbHeading
|
|
1032
|
+
// - Accordion: AccordionItem, AccordionContent, AccordionToggle
|
|
1033
|
+
// - Card: CardBody, CardHeader, CardTitle
|
|
1034
|
+
// - ActionList: ActionListItem
|
|
1035
|
+
// - Modal: ModalContent, ModalHeader
|
|
1036
|
+
// - DataList: DataListAction (only this one is skipped - others get full attributes)
|
|
1037
|
+
// - DescriptionList: None skipped - all children get attributes (they have meaningful variants/states)
|
|
1038
|
+
// - Form: FormGroup, FormSection (if they exist)
|
|
1039
|
+
const structuralChildren = [
|
|
1040
|
+
'breadcrumbitem', 'breadcrumbheading',
|
|
1041
|
+
'accordionitem', 'accordioncontent', 'accordiontoggle',
|
|
1042
|
+
'cardbody', 'cardheader', 'cardtitle',
|
|
1043
|
+
'actionlistitem',
|
|
1044
|
+
'modalcontent', 'modalheader', 'modalfooter', 'modalbody',
|
|
1045
|
+
// Modal structural children - ModalContent, ModalHeader, ModalFooter, ModalBody are structural
|
|
1046
|
+
'datalistaction', // Only DataListAction is skipped, other DataList children get attributes
|
|
1047
|
+
// DescriptionList children are NOT skipped - they have meaningful variants/states
|
|
1048
|
+
// Droppable and Draggable are NOT skipped - they get all attributes except role (handled by parent)
|
|
1049
|
+
'drawermain', 'drawerpanel', 'drawercontent', 'drawerbody',
|
|
1050
|
+
'drawerhead', 'draweractions', 'drawersection', 'drawersectiongroup',
|
|
1051
|
+
// Drawer structural children - role and purpose handled by parent Drawer
|
|
1052
|
+
'duallistselectorlist', // DualListSelectorList is purely structural - skipped
|
|
1053
|
+
// DualListSelectorPane and DualListSelectorListItem get attributes (they have meaningful variants/states)
|
|
1054
|
+
'emptystateheader', 'emptystateicon', 'emptystatebody',
|
|
1055
|
+
'emptystatefooter', 'emptystateactions',
|
|
1056
|
+
// EmptyState structural children - role and purpose handled by parent EmptyState
|
|
1057
|
+
// Structural children: EmptyStateHeader, EmptyStateIcon, EmptyStateBody, EmptyStateFooter, EmptyStateActions
|
|
1058
|
+
// Note: Nested components like Button and Spinner are NOT skipped - they get their own attributes
|
|
1059
|
+
// (they are independent components used within EmptyState, not structural children)
|
|
1060
|
+
'expandablesectiontoggle', 'expandablesectioncontent',
|
|
1061
|
+
// ExpandableSection structural children - role and purpose handled by parent ExpandableSection
|
|
1062
|
+
// Structural children: ExpandableSectionToggle, ExpandableSectionContent
|
|
1063
|
+
'helpertext', 'helper-text',
|
|
1064
|
+
// HelperText is a structural child of form inputs - its state is reflected in the input's variant/state
|
|
1065
|
+
'mastheadbrand',
|
|
1066
|
+
// MastheadBrand is a structural wrapper for Brand component - skipped
|
|
1067
|
+
'menulist', 'menugroup', 'menusearch', 'menusearchinput',
|
|
1068
|
+
// Menu structural children - MenuList, MenuGroup, MenuSearch, MenuSearchInput are structural
|
|
1069
|
+
'selectoptiongroup',
|
|
1070
|
+
// SelectOptionGroup is a structural wrapper for grouping select options
|
|
1071
|
+
'notificationdrawerheader', 'notificationdrawerbody',
|
|
1072
|
+
// NotificationDrawer structural children - NotificationDrawerHeader, NotificationDrawerBody are structural
|
|
1073
|
+
'overflowmenucontent',
|
|
1074
|
+
// OverflowMenuContent is a structural wrapper for menu items
|
|
1075
|
+
// Note: PageSection, PageHeader, PageBody, PageFooter are NOT structural - they get their own attributes
|
|
1076
|
+
'wizardnav', 'wizardbody', 'wizardfooter',
|
|
1077
|
+
// Wizard structural children - WizardNav, WizardBody, WizardFooter are structural
|
|
1078
|
+
// WizardNavItem is NOT structural - it gets its own attributes (visited, current, disabled states)
|
|
1079
|
+
];
|
|
1080
|
+
|
|
1081
|
+
if (structuralChildren.some(child => name.includes(child))) {
|
|
1082
|
+
return;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
// Note: Independent components (Button, Link, etc.) keep their attributes
|
|
1086
|
+
// even when nested, because they can be used independently
|
|
1087
|
+
|
|
1088
|
+
// Skip if already has semantic attributes (don't duplicate)
|
|
1089
|
+
const existingAttrs = node.attributes || [];
|
|
1090
|
+
if (hasSemanticAttributes(existingAttrs)) {
|
|
1091
|
+
return;
|
|
1092
|
+
}
|
|
1093
|
+
|
|
1094
|
+
// Find parent context for nested components
|
|
1095
|
+
const parentContext = findParentContext(path, imports);
|
|
1096
|
+
|
|
1097
|
+
// Create and add semantic attributes
|
|
1098
|
+
const newAttributes = createSemanticAttributes(j, componentName, existingAttrs, parentContext, path);
|
|
1099
|
+
node.attributes = [...existingAttrs, ...newAttributes];
|
|
1100
|
+
});
|
|
1101
|
+
|
|
1102
|
+
return root.toSource({
|
|
1103
|
+
quote: 'single',
|
|
1104
|
+
trailingComma: true,
|
|
1105
|
+
lineTerminator: '\n',
|
|
1106
|
+
});
|
|
1107
|
+
};
|
|
1108
|
+
|