Skip to content

Commit 21e5f92

Browse files
authored
chore(hooks): Refactor styling hooks automated metadata extraction (#4944)
* chore(hooks): Refactor styling hooks automated metadata extraction * feat(hooks): Display hook names in ascending alpha order
1 parent 4c4f061 commit 21e5f92

File tree

2 files changed

+130
-33
lines changed

2 files changed

+130
-33
lines changed

scripts/var-extract.js

Lines changed: 109 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -77,12 +77,16 @@ const extractVarsFromCSS = (cssContent, options = {}) => {
7777
const { allowPattern } = options;
7878
let list = {};
7979

80+
// retrieve the category names from the JSON config
81+
const categories = Object.keys(propTypes);
82+
8083
rules.map((rule) => {
84+
// Filter rule declarations to only those that contain at least one `var(...)` function
8185
const filtered = rule.declarations.filter((dec) => {
8286
if (!dec.value) return false;
8387

84-
// match on var values that are not custom props
85-
if (dec.value.match(/var\(/) && !dec.property.match(/^--/)) {
88+
// match on property values that contain at least one `var(...)` function
89+
if (dec.value.match(/var\(/)) {
8690
return true;
8791
}
8892

@@ -111,23 +115,102 @@ const extractVarsFromCSS = (cssContent, options = {}) => {
111115
// \) -> a closing parenthesis
112116
const varRegex = /var\((?=\S*['-]*)([a-zA-Z'-]+)\s*[,]?\s*(.*)\)/;
113117

118+
// Sanitize a CSS property value
119+
// - currently only removes extra content at the end of the value when closing parentheses aren't balanced
120+
function sanitizeValue(value) {
121+
let sanitizedValue = value;
122+
let parens = 0;
123+
let startParens = 0;
124+
let endParens = 0;
125+
126+
// check each character of the value string to count opening & closing parentheses
127+
for (let pos = 0; pos <= value.length; pos++) {
128+
let char = value.charAt(pos);
129+
switch (char) {
130+
// increment parenthesis counter and startParens counter
131+
case '(':
132+
parens++;
133+
startParens++;
134+
break;
135+
// decrement parenthesis counter and increment endParens counter
136+
case ')':
137+
parens--;
138+
endParens++;
139+
break;
140+
}
141+
142+
// if we have unbalanced parentheses then sanitize
143+
if (parens !== 0) {
144+
// if we have too many closing parentheses then sanitize things at the end
145+
if (endParens > startParens) {
146+
// find the index of the last closing balanced parenthesis
147+
const closingParens = value.matchAll(/\)/g);
148+
149+
// convert iterator object to an array
150+
const closingParensArray = [];
151+
for (const match of closingParens) {
152+
closingParensArray.push(match);
153+
}
154+
155+
// sort the array by ascending match index/position
156+
closingParensArray.sort((a, b) => {
157+
if (a.index < b.index) {
158+
return -1;
159+
}
160+
if (a.index > b.index) {
161+
return 1;
162+
}
163+
return 0;
164+
});
165+
166+
// get the index of the balanced closing parenthesis we want to keep
167+
const indexOfBalancedParen =
168+
closingParensArray.length - (endParens - startParens) - 1; // -1 to make it a zero based index
169+
const balancedClosingParenIndex =
170+
closingParensArray[indexOfBalancedParen].index;
171+
172+
// remove the extra content at the end that has no opening parentheses to match
173+
sanitizedValue = value.slice(0, balancedClosingParenIndex + 1); // +1 to avoid losing the parenthesis we want to keep
174+
}
175+
}
176+
}
177+
178+
return sanitizedValue;
179+
}
180+
114181
/**
115182
* Recursively drill into a CSS value to find the fallback value
116183
*
117184
* @param {string} - CSS property's value
118185
* @returns {object} - hookName, name of the hook; fallback, value of the fallback
119186
*/
120-
function findFallbackRecursively(value) {
187+
function findFallbackRecursively(value, recursionIndex = 0) {
121188
// get the fallback value from the CSS variable clause
122189
let regex = varRegex;
123-
let matches = value.match(regex);
124190

191+
// do an initial match check
192+
const initialMatches = value.match(regex);
193+
let matches;
194+
195+
// we have a match
196+
if (initialMatches) {
197+
// sanitize the returned match value found to ensure we're digging into a properly formatted var(...) value
198+
const sanitizedValue = sanitizeValue(initialMatches[0]);
199+
matches = sanitizedValue.match(regex);
200+
}
201+
202+
// if we found a match then begin the recursion
125203
if (matches) {
126204
let fallbackValue = matches[2];
127205

206+
// if we're not on the first iteration then add the valid hook to the list
207+
if (recursionIndex > 0) {
208+
addHookToList(matches[1], null);
209+
}
210+
128211
// if we have another CSS variable clause then keep drilling in
129212
if (isCssVarClause(fallbackValue)) {
130-
return findFallbackRecursively(fallbackValue);
213+
return findFallbackRecursively(fallbackValue, recursionIndex++);
131214
} else {
132215
return matches[2].trim();
133216
}
@@ -142,6 +225,26 @@ const extractVarsFromCSS = (cssContent, options = {}) => {
142225
}
143226
///
144227

228+
// Add hook to the list to be shown
229+
function addHookToList(hookName, fallbackValue) {
230+
// do we have a category match? If so return it
231+
const matchedCategory = categories.find((option) => {
232+
if (hookName.includes(option)) {
233+
return option;
234+
}
235+
});
236+
237+
// add the fallback value to the hook's object in the list of results
238+
list[hookName] = { fallbackValue: fallbackValue };
239+
240+
// if a category was found add the metadata to the hook's object in the list of results
241+
if (matchedCategory) {
242+
list[hookName].category = propTypes[matchedCategory].type;
243+
list[hookName].valueType =
244+
propTypes[matchedCategory].valueTypes.join(', ');
245+
}
246+
}
247+
145248
if (filtered.length > 0) {
146249
filtered.forEach((decl) => {
147250
if (decl.value) {
@@ -154,25 +257,7 @@ const extractVarsFromCSS = (cssContent, options = {}) => {
154257

155258
// if we have a hook then generate an object to add to the list of results
156259
if (hookName) {
157-
// retrieve the category names from the JSON config
158-
const categories = Object.keys(propTypes);
159-
160-
// do we have a category match? If so return it
161-
const matchedCategory = categories.find((option) => {
162-
if (hookName.includes(option)) {
163-
return option;
164-
}
165-
});
166-
167-
// add the fallback value to the hook's object in the list of results
168-
list[hookName] = { fallbackValue: fallback };
169-
170-
// if a category was found add the metadata to the hook's object in the list of results
171-
if (matchedCategory) {
172-
list[hookName].category = propTypes[matchedCategory].type;
173-
list[hookName].valueType =
174-
propTypes[matchedCategory].valueTypes.join(', ');
175-
}
260+
addHookToList(hookName, fallback);
176261
}
177262
}
178263
});

shared/components/StylingHooksTable.jsx

Lines changed: 21 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ const getPropTypeData = (name, varName) => {
1212
let propType = null;
1313
let valueTypes = null;
1414

15-
Object.keys(propTypes).forEach(prop => {
15+
Object.keys(propTypes).forEach((prop) => {
1616
if (trimmedVar.match(prop)) {
1717
propType = propTypes[prop].type;
1818
valueTypes = propTypes[prop].valueTypes;
@@ -21,11 +21,11 @@ const getPropTypeData = (name, varName) => {
2121

2222
return {
2323
propType,
24-
valueTypes
24+
valueTypes,
2525
};
2626
};
2727

28-
const formatType = type => {
28+
const formatType = (type) => {
2929
let formattedType = type.toLowerCase();
3030
if (formattedType === 'color') {
3131
formattedType += '_value'; // MDN adds _value to color data type page, this discrepancy is unique
@@ -46,7 +46,7 @@ class StylingHooksTable extends Component {
4646
}
4747

4848
const categories = Object.keys(vars)
49-
.map(varName => {
49+
.map((varName) => {
5050
return getPropTypeData(name, varName).propType;
5151
})
5252
.filter((value, i, self) => self.indexOf(value) === i)
@@ -78,15 +78,27 @@ class StylingHooksTable extends Component {
7878
{categories.map((category, i) => {
7979
const categoryVars = Object.keys(vars)
8080
.filter(
81-
varName =>
81+
(varName) =>
8282
getPropTypeData(name, varName).propType === category
8383
)
84-
.map(varName => {
84+
.map((varName) => {
8585
return {
8686
name: varName,
8787
value: vars[varName].fallbackValue,
88-
types: getPropTypeData(name, varName).valueTypes || []
88+
types: getPropTypeData(name, varName).valueTypes || [],
8989
};
90+
})
91+
.sort((a, b) => {
92+
const nameA = a.name;
93+
const nameB = b.name;
94+
95+
if (nameA < nameB) {
96+
return -1; // nameA comes first
97+
}
98+
if (nameA > nameB) {
99+
return 1; // nameB comes first
100+
}
101+
return 0; // names must be equal
90102
});
91103

92104
return (
@@ -97,7 +109,7 @@ class StylingHooksTable extends Component {
97109
className={classNames({
98110
'hooks-table__section': ii === 0,
99111
'hooks-table__section_end':
100-
ii === categoryVars.length - 1
112+
ii === categoryVars.length - 1,
101113
})}
102114
>
103115
{ii === 0 && (
@@ -135,7 +147,7 @@ class StylingHooksTable extends Component {
135147
{type}
136148
</a>
137149
{/* Proper comma separation when there is more than 1 valueType */}
138-
{(arr.length > 1 && x <= 0) && (', ')}
150+
{arr.length > 1 && x <= 0 && ', '}
139151
</>
140152
);
141153
})}

0 commit comments

Comments
 (0)