-
Notifications
You must be signed in to change notification settings - Fork 24.4k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
feat: linear gradient px
and transition hint syntax support
#48410
base: main
Are you sure you want to change the base?
Changes from 2 commits
f65c873
8bbcc0a
6913cdb
3c443c3
a21b3e6
a1048a2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -22,11 +22,13 @@ type LinearGradientDirection = | |
| {type: 'angle', value: number} | ||
| {type: 'keyword', value: string}; | ||
|
||
type ColorStopColor = ProcessedColorValue | null; | ||
|
||
type ParsedGradientValue = { | ||
type: 'linearGradient', | ||
direction: LinearGradientDirection, | ||
colorStops: $ReadOnlyArray<{ | ||
color: ProcessedColorValue, | ||
color: ColorStopColor, | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I would comment in what scenarios we can have a null color, or otherwise change the typing to make that super clear like
I think they are similar enough to just roll with the comment but it should be clear that that syntax is supported There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Added a comment. Thanks! |
||
position: number, | ||
}>, | ||
}; | ||
|
@@ -49,33 +51,52 @@ export default function processBackgroundImage( | |
} else if (Array.isArray(backgroundImage)) { | ||
for (const bgImage of backgroundImage) { | ||
const processedColorStops: Array<{ | ||
color: ProcessedColorValue, | ||
color: ColorStopColor, | ||
position: number | null, | ||
}> = []; | ||
for (let index = 0; index < bgImage.colorStops.length; index++) { | ||
const colorStop = bgImage.colorStops[index]; | ||
const processedColor = processColor(colorStop.color); | ||
if (processedColor == null) { | ||
// If a color is invalid, return an empty array and do not apply gradient. Same as web. | ||
return []; | ||
} | ||
if (colorStop.positions != null && colorStop.positions.length > 0) { | ||
for (const position of colorStop.positions) { | ||
if (position.endsWith('%')) { | ||
processedColorStops.push({ | ||
color: processedColor, | ||
position: parseFloat(position) / 100, | ||
}); | ||
} else { | ||
// If a position is invalid, return an empty array and do not apply gradient. Same as web. | ||
return []; | ||
} | ||
const positions = colorStop.positions; | ||
// Color transition hint syntax (red, 20%, blue) | ||
if ( | ||
colorStop.color == null && | ||
Array.isArray(positions) && | ||
positions.length === 1 | ||
) { | ||
const position = positions[0]; | ||
if (typeof position === 'string' && position.endsWith('%')) { | ||
processedColorStops.push({ | ||
color: null, | ||
position: parseFloat(position) / 100, | ||
}); | ||
} else { | ||
// If a position is invalid, return an empty array and do not apply gradient. Same as web. | ||
return []; | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think we can change this to share some code better. Do transition hints have to be percentages? Regardless we are doing a lot of the same things, can you try to get rid of the top level if/else here and share some of this logic? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
i kinda prefer the explicit checks, also each branch has a tiny comment on top, but i will take another look! |
||
} | ||
} else { | ||
processedColorStops.push({ | ||
color: processedColor, | ||
position: null, | ||
}); | ||
const processedColor = processColor(colorStop.color); | ||
if (processedColor == null) { | ||
// If a color is invalid, return an empty array and do not apply gradient. Same as web. | ||
return []; | ||
} | ||
if (positions != null && positions.length > 0) { | ||
for (const position of positions) { | ||
if (position.endsWith('%')) { | ||
processedColorStops.push({ | ||
color: processedColor, | ||
position: parseFloat(position) / 100, | ||
}); | ||
} else { | ||
// If a position is invalid, return an empty array and do not apply gradient. Same as web. | ||
return []; | ||
} | ||
} | ||
} else { | ||
processedColorStops.push({ | ||
color: processedColor, | ||
position: null, | ||
}); | ||
} | ||
} | ||
} | ||
|
||
|
@@ -169,47 +190,85 @@ function parseCSSLinearGradient( | |
// If first part is not an angle/direction or a color stop, return an empty array and do not apply any gradient. Same as web. | ||
return []; | ||
} | ||
colorStopRegex.lastIndex = 0; | ||
|
||
const colorStopsString = parts.join(','); | ||
const colorStops = []; | ||
const fullColorStopsStr = parts.join(','); | ||
let colorStopMatch; | ||
while ((colorStopMatch = colorStopRegex.exec(fullColorStopsStr))) { | ||
const [, color, position1, position2] = colorStopMatch; | ||
const processedColor = processColor(color.trim().toLowerCase()); | ||
if (processedColor == null) { | ||
// If a color is invalid, return an empty array and do not apply any gradient. Same as web. | ||
// split by comma, but not if it's inside a parentheses. e.g. red, rgba(0, 0, 0, 0.5), green => ["red", "rgba(0, 0, 0, 0.5)", "green"] | ||
const stops = colorStopsString.split(/,(?![^(]*\))/); | ||
for (const stop of stops) { | ||
const trimmedStop = stop.trim().toLowerCase(); | ||
// Match function like pattern or single words | ||
const colorStopParts = trimmedStop.match(/\S+\([^)]*\)|\S+/g); | ||
if (colorStopParts == null) { | ||
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web. | ||
return []; | ||
} | ||
|
||
if (typeof position1 !== 'undefined') { | ||
if (position1.endsWith('%')) { | ||
// Case 1: [color, position, position] | ||
if (colorStopParts.length === 3) { | ||
const color = colorStopParts[0]; | ||
const position1 = colorStopParts[1]; | ||
const position2 = colorStopParts[2]; | ||
const processedColor = processColor(color); | ||
if (processedColor == null) { | ||
// If a color is invalid, return an empty array and do not apply any gradient. Same as web. | ||
return []; | ||
} | ||
if (position1.endsWith('%') && position2.endsWith('%')) { | ||
colorStops.push({ | ||
color: processedColor, | ||
position: parseFloat(position1) / 100, | ||
}); | ||
colorStops.push({ | ||
color: processedColor, | ||
position: parseFloat(position2) / 100, | ||
}); | ||
} else { | ||
// If a position is invalid, return an empty array and do not apply any gradient. Same as web. | ||
return []; | ||
} | ||
} else { | ||
colorStops.push({ | ||
color: processedColor, | ||
position: null, | ||
}); | ||
} | ||
|
||
if (typeof position2 !== 'undefined') { | ||
if (position2.endsWith('%')) { | ||
// Case 2: [color, position] | ||
else if (colorStopParts.length === 2) { | ||
const color = colorStopParts[0]; | ||
const position = colorStopParts[1]; | ||
const processedColor = processColor(color); | ||
if (processedColor == null) { | ||
// If a color is invalid, return an empty array and do not apply any gradient. Same as web. | ||
return []; | ||
} | ||
if (position.endsWith('%')) { | ||
colorStops.push({ | ||
color: processedColor, | ||
position: parseFloat(position2) / 100, | ||
position: parseFloat(position) / 100, | ||
}); | ||
} else { | ||
// If a position is invalid, return an empty array and do not apply any gradient. Same as web. | ||
return []; | ||
} | ||
} | ||
// Case 3: [color] | ||
// Case 4: [position] => transition hint syntax | ||
else if (colorStopParts.length === 1) { | ||
if (colorStopParts[0].endsWith('%')) { | ||
colorStops.push({ | ||
color: null, | ||
position: parseFloat(colorStopParts[0]) / 100, | ||
}); | ||
} else { | ||
const processedColor = processColor(colorStopParts[0]); | ||
if (processedColor == null) { | ||
// If a color is invalid, return an empty array and do not apply any gradient. Same as web. | ||
return []; | ||
} | ||
colorStops.push({ | ||
color: processedColor, | ||
position: null, | ||
}); | ||
} | ||
} else { | ||
// If a color stop is invalid, return an empty array and do not apply any gradient. Same as web. | ||
return []; | ||
} | ||
} | ||
|
||
const fixedColorStops = getFixedColorStops(colorStops); | ||
|
@@ -286,15 +345,15 @@ function getAngleInDegrees(angle?: string): ?number { | |
// https://drafts.csswg.org/css-images-4/#color-stop-fixup | ||
function getFixedColorStops( | ||
colorStops: $ReadOnlyArray<{ | ||
color: ProcessedColorValue, | ||
color: ColorStopColor, | ||
position: number | null, | ||
}>, | ||
): Array<{ | ||
color: ProcessedColorValue, | ||
color: ColorStopColor, | ||
position: number, | ||
}> { | ||
let fixedColorStops: Array<{ | ||
color: ProcessedColorValue, | ||
color: ColorStopColor, | ||
position: number, | ||
}> = []; | ||
let hasNullPositions = false; | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
can you add some more? Hints that are invalid (> 100%, non percents, multiple in a row, etc). Some longer ones with more stops and more hints just to stress test things a bit, etc. The code has a lot of branching logic due to the complexity of the syntax we are allowing, so I think we should err on the side of excessive testing to raise our confidence level here
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Agree, added.