@bravostudioai/react 0.1.28 → 0.1.30

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.
@@ -0,0 +1,284 @@
1
+ /**
2
+ * Reusable prop name qualification utilities
3
+ *
4
+ * When multiple components/inputs have the same base prop name,
5
+ * this module provides utilities to make each name unique by using
6
+ * minimal distinguishing parent paths.
7
+ */
8
+
9
+ import { generateQualifiedPropName, findMinimalDistinguishingPath, arraysEqual } from './parser';
10
+
11
+ /**
12
+ * Base interface for items that can have their prop names qualified
13
+ */
14
+ export interface QualifiableItem {
15
+ id: string;
16
+ name: string;
17
+ propName: string;
18
+ _parentPath?: string[];
19
+ }
20
+
21
+ /**
22
+ * Groups items by their prop name
23
+ */
24
+ function groupByPropName<T extends QualifiableItem>(
25
+ items: T[]
26
+ ): Map<string, T[]> {
27
+ const groups = new Map<string, T[]>();
28
+
29
+ items.forEach((item) => {
30
+ const baseName = item.propName;
31
+ if (!groups.has(baseName)) {
32
+ groups.set(baseName, []);
33
+ }
34
+ groups.get(baseName)!.push(item);
35
+ });
36
+
37
+ return groups;
38
+ }
39
+
40
+ /**
41
+ * Applies minimal distinguishing paths to a group of items with duplicate names
42
+ */
43
+ function applyMinimalPaths<T extends QualifiableItem>(group: T[]): void {
44
+ group.forEach((item) => {
45
+ const otherPaths = group
46
+ .filter((other) => other.id !== item.id)
47
+ .map((other) => other._parentPath || []);
48
+
49
+ const minimalPath = findMinimalDistinguishingPath(
50
+ item._parentPath || [],
51
+ otherPaths
52
+ );
53
+
54
+ item.propName = generateQualifiedPropName(
55
+ item.name || 'item',
56
+ minimalPath
57
+ );
58
+ });
59
+ }
60
+
61
+ /**
62
+ * Expands distinguishing paths iteratively until all names are unique
63
+ */
64
+ function expandPathsUntilUnique<T extends QualifiableItem>(
65
+ group: T[],
66
+ allItems: T[]
67
+ ): void {
68
+ let hasDuplicates = true;
69
+ let iteration = 0;
70
+ const maxIterations = 10; // Safety limit
71
+
72
+ while (hasDuplicates && iteration < maxIterations) {
73
+ iteration++;
74
+
75
+ // Group by current qualified names
76
+ const qualifiedNameGroups = new Map<string, T[]>();
77
+ group.forEach((item) => {
78
+ if (!qualifiedNameGroups.has(item.propName)) {
79
+ qualifiedNameGroups.set(item.propName, []);
80
+ }
81
+ qualifiedNameGroups.get(item.propName)!.push(item);
82
+ });
83
+
84
+ hasDuplicates = false;
85
+
86
+ // For each group of duplicated qualified names, expand their paths
87
+ qualifiedNameGroups.forEach((dupGroup) => {
88
+ if (dupGroup.length > 1) {
89
+ hasDuplicates = true;
90
+
91
+ dupGroup.forEach((item) => {
92
+ const fullPath = item._parentPath || [];
93
+ const otherFullPaths = dupGroup
94
+ .filter((other) => other.id !== item.id)
95
+ .map((other) => other._parentPath || []);
96
+
97
+ // Find common prefix length
98
+ let commonPrefixLength = 0;
99
+ const maxCommonLength = Math.min(
100
+ fullPath.length,
101
+ ...otherFullPaths.map((p) => p.length)
102
+ );
103
+
104
+ for (let i = 0; i < maxCommonLength; i++) {
105
+ const thisPart = fullPath[i];
106
+ const allMatch = otherFullPaths.every((otherPath) => otherPath[i] === thisPart);
107
+ if (allMatch) {
108
+ commonPrefixLength++;
109
+ } else {
110
+ break;
111
+ }
112
+ }
113
+
114
+ // Use progressively more of the distinguishing suffix
115
+ const distinguishingSuffix = fullPath.slice(commonPrefixLength);
116
+
117
+ // Try expanding until unique
118
+ let foundUnique = false;
119
+ for (
120
+ let suffixLength = 1;
121
+ suffixLength <= distinguishingSuffix.length;
122
+ suffixLength++
123
+ ) {
124
+ const expandedPath = distinguishingSuffix.slice(0, suffixLength);
125
+ const testQualifiedName = generateQualifiedPropName(
126
+ item.name || 'item',
127
+ expandedPath
128
+ );
129
+
130
+ // Check if unique among ALL items
131
+ const isUnique = allItems.every((otherItem) => {
132
+ if (otherItem.id === item.id) return true;
133
+
134
+ // If in same duplicate group, compare expanded paths
135
+ if (dupGroup.some((d) => d.id === otherItem.id)) {
136
+ const otherFullPath = otherItem._parentPath || [];
137
+ const otherCommonPrefixLength = Math.min(
138
+ commonPrefixLength,
139
+ otherFullPath.length
140
+ );
141
+ const otherDistinguishingSuffix = otherFullPath.slice(
142
+ otherCommonPrefixLength
143
+ );
144
+ const otherExpandedPath = otherDistinguishingSuffix.slice(
145
+ 0,
146
+ suffixLength
147
+ );
148
+ const otherQualifiedName = generateQualifiedPropName(
149
+ otherItem.name || 'item',
150
+ otherExpandedPath
151
+ );
152
+ return testQualifiedName !== otherQualifiedName;
153
+ }
154
+
155
+ // For items outside duplicate group, check final prop name
156
+ return testQualifiedName !== otherItem.propName;
157
+ });
158
+
159
+ if (isUnique) {
160
+ item.propName = testQualifiedName;
161
+ foundUnique = true;
162
+ break;
163
+ }
164
+ }
165
+
166
+ // If still not unique, use the distinguishing suffix we found
167
+ if (!foundUnique) {
168
+ item.propName = generateQualifiedPropName(
169
+ item.name || 'item',
170
+ distinguishingSuffix.length > 0 ? distinguishingSuffix : []
171
+ );
172
+ }
173
+ });
174
+ }
175
+ });
176
+ }
177
+ }
178
+
179
+ /**
180
+ * Applies numeric suffixes as a last resort for items with identical paths
181
+ */
182
+ function applyNumericSuffixes<T extends QualifiableItem>(group: T[]): void {
183
+ // Group by final qualified names
184
+ const finalQualifiedNameGroups = new Map<string, T[]>();
185
+ group.forEach((item) => {
186
+ if (!finalQualifiedNameGroups.has(item.propName)) {
187
+ finalQualifiedNameGroups.set(item.propName, []);
188
+ }
189
+ finalQualifiedNameGroups.get(item.propName)!.push(item);
190
+ });
191
+
192
+ finalQualifiedNameGroups.forEach((finalDupGroup, finalQualifiedName) => {
193
+ if (finalDupGroup.length > 1) {
194
+ // Check if all duplicates have identical paths
195
+ const allPathsIdentical = finalDupGroup.every((item) => {
196
+ const thisPath = item._parentPath || [];
197
+ return finalDupGroup.every((otherItem) => {
198
+ if (otherItem.id === item.id) return true;
199
+ const otherPath = otherItem._parentPath || [];
200
+ return arraysEqual(thisPath, otherPath);
201
+ });
202
+ });
203
+
204
+ // Only use numeric suffixes if paths are truly identical
205
+ if (allPathsIdentical) {
206
+ finalDupGroup.forEach((item, index) => {
207
+ if (index > 0) {
208
+ item.propName = `${finalQualifiedName}${index + 1}`;
209
+ }
210
+ });
211
+ }
212
+ }
213
+ });
214
+ }
215
+
216
+ /**
217
+ * Cleans up temporary _parentPath property from an item
218
+ */
219
+ function cleanupParentPath<T extends QualifiableItem>(item: T): void {
220
+ delete item._parentPath;
221
+ }
222
+
223
+ /**
224
+ * Qualifies a single group of items with duplicate prop names
225
+ */
226
+ function qualifyGroup<T extends QualifiableItem>(
227
+ group: T[],
228
+ allItems: T[]
229
+ ): void {
230
+ if (group.length === 1) {
231
+ cleanupParentPath(group[0]);
232
+ return;
233
+ }
234
+
235
+ // Step 1: Apply minimal distinguishing paths
236
+ applyMinimalPaths(group);
237
+
238
+ // Step 2: Iteratively expand paths until all are unique
239
+ expandPathsUntilUnique(group, allItems);
240
+
241
+ // Step 3: Apply numeric suffixes as last resort
242
+ applyNumericSuffixes(group);
243
+
244
+ // Step 4: Cleanup temporary properties
245
+ group.forEach(cleanupParentPath);
246
+ }
247
+
248
+ /**
249
+ * Qualifies duplicate prop names using minimal distinguishing paths.
250
+ *
251
+ * When multiple components have the same prop name, this function
252
+ * finds the shortest parent path segment that makes each name unique.
253
+ *
254
+ * Algorithm:
255
+ * 1. Group items by base prop name
256
+ * 2. For each group with duplicates:
257
+ * a. Find minimal distinguishing paths
258
+ * b. Iteratively expand paths if duplicates remain
259
+ * c. Apply numeric suffixes if paths are identical
260
+ *
261
+ * @param items - Components/inputs to qualify (will be modified in place)
262
+ * @returns The same items array with qualified propName values
263
+ *
264
+ * @example
265
+ * const items = [
266
+ * { id: '1', name: 'Title', propName: 'title', _parentPath: ['Header'] },
267
+ * { id: '2', name: 'Title', propName: 'title', _parentPath: ['Footer'] }
268
+ * ];
269
+ * qualifyPropNames(items);
270
+ * // Result: items[0].propName = 'headerTitle', items[1].propName = 'footerTitle'
271
+ */
272
+ export function qualifyPropNames<T extends QualifiableItem>(
273
+ items: T[]
274
+ ): T[] {
275
+ if (items.length === 0) return items;
276
+
277
+ const propNameGroups = groupByPropName(items);
278
+
279
+ propNameGroups.forEach((group) => {
280
+ qualifyGroup(group, items);
281
+ });
282
+
283
+ return items;
284
+ }