in my css file I have gradient rule, like this:
background-image:linear-gradient(to right, #FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%);
Parsing CSS can be far more complex there are few things to remember:
red
, blue
, etc).OK, so here is a crazy example of how you "could" parse the gradient using regular expressions - I'm not saying you should.
Here I build my regular expressions in code to keep some level of readability and maintainability of the code.
The final output of the test_this_thing
functions console.log(result);
is as follows:
Input:
background-image:linear-gradient(to right bottom, #FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%);
Output:
{
original:"to right bottom, #FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%",
line:"to right bottom",
sideCorner:"right bottom",
colorStopList:[
{
color:"#FF0000",
position:"0%"
},
{
color:"#00FF00",
position:"20px"
},
{
color:"rgb(0, 0, 255)",
position:"100%"
}
]
}
Note the output includes the original
property - this looks like the input - but if part of the input wasn't matched the input
and original
values would be different; noting an possible error in the parser.
Here is a source:
/**
* Utility combine multiple regular expressions.
*
* @param {RegExp[]|string[]} regexpList List of regular expressions or strings.
* @param {string} flags Normal RegExp flags.
*/
var combineRegExp = function (regexpList, flags) {
var i,
source = '';
for (i = 0; i < regexpList.length; i++) {
if (typeof regexpList[i] === 'string') {
source += regexpList[i];
} else {
source += regexpList[i].source;
}
}
return new RegExp(source, flags);
};
/**
* Generate the required regular expressions once.
*
* Regular Expressions are easier to manage this way and can be well described.
*
* @result {object} Object containing regular expressions.
*/
var generateRegExp = function () {
// Note any variables with "Capture" in name include capturing bracket set(s).
var searchFlags = 'gi', // ignore case for angles, "rgb" etc
rAngle = /(?:[+-]?\d*\.?\d+)(?:deg|grad|rad|turn)/, // Angle +ive, -ive and angle types
rSideCornerCapture = /to\s+((?:(?:left|right)(?:\s+(?:top|bottom))?))/, // optional 2nd part
rComma = /\s*,\s*/, // Allow space around comma.
rColorHex = /\#(?:[a-f0-9]{6}|[a-f0-9]{3})/, // 3 or 6 character form
rDigits3 = /\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}\s*\)/,// "(1, 2, 3)"
rDigits4 = /\(\s*(?:\d{1,3}\s*,\s*){2}\d{1,3}\s*,\s*\d*\.?\d+\)/,// "(1, 2, 3, 4)"
rValue = /(?:[+-]?\d*\.?\d+)(?:%|[a-z]+)?/,// ".9", "-5px", "100%".
rKeyword = /[_a-z-][_a-z0-9-]*/,// "red", "transparent", "border-collapse".
rColor = combineRegExp([
'(?:', rColorHex, '|', '(?:rgb|hsl)', rDigits3, '|', '(?:rgba|hsla)', rDigits4, '|', rKeyword, ')'
], ''),
rColorStop = combineRegExp([rColor, '(?:\\s+', rValue, '(?:\\s+', rValue, ')?)?'], ''),// Single Color Stop, optional %, optional length.
rColorStopList = combineRegExp(['(?:', rColorStop, rComma, ')*', rColorStop], ''),// List of color stops min 1.
rLineCapture = combineRegExp(['(?:(', rAngle, ')|', rSideCornerCapture, ')'], ''),// Angle or SideCorner
rGradientSearch = combineRegExp([
'(?:(', rLineCapture, ')', rComma, ')?(', rColorStopList, ')'
], searchFlags),// Capture 1:"line", 2:"angle" (optional), 3:"side corner" (optional) and 4:"stop list".
rColorStopSearch = combineRegExp([
'\\s*(', rColor, ')', '(?:\\s+', '(', rValue, '))?', '(?:', rComma, '\\s*)?'
], searchFlags);// Capture 1:"color" and 2:"position" (optional).
return {
gradientSearch: rGradientSearch,
colorStopSearch: rColorStopSearch
};
};
/**
* Actually parse the input gradient parameters string into an object for reusability.
*
*
* @note Really this only supports the standard syntax not historical versions, see MDN for details
* https://developer.mozilla.org/en-US/docs/Web/CSS/linear-gradient
*
* @param regExpLib
* @param {string} input Input string in the form "to right bottom, #FF0 0%, red 20px, rgb(0, 0, 255) 100%"
* @returns {object|undefined} Object containing break down of input string including array of stop points.
*/
var parseGradient = function (regExpLib, input) {
var result,
matchGradient,
matchColorStop,
stopResult;
// reset search position, because we reuse regex.
regExpLib.gradientSearch.lastIndex = 0;
matchGradient = regExpLib.gradientSearch.exec(input);
if (matchGradient !== null) {
result = {
original: matchGradient[0],
colorStopList: []
};
// Line (Angle or Side-Corner).
if (!!matchGradient[1]) {
result.line = matchGradient[1];
}
// Angle or undefined if side-corner.
if (!!matchGradient[2]) {
result.angle = matchGradient[2];
}
// Side-corner or undefined if angle.
if (!!matchGradient[3]) {
result.sideCorner = matchGradient[3];
}
// reset search position, because we reuse regex.
regExpLib.colorStopSearch.lastIndex = 0;
// Loop though all the color-stops.
matchColorStop = regExpLib.colorStopSearch.exec(matchGradient[4]);
while (matchColorStop !== null) {
stopResult = {
color: matchColorStop[1]
};
// Position (optional).
if (!!matchColorStop[2]) {
stopResult.position = matchColorStop[2];
}
result.colorStopList.push(stopResult);
// Continue searching from previous position.
matchColorStop = regExpLib.colorStopSearch.exec(matchGradient[4]);
}
}
// Can be undefined if match not found.
return result;
};
var test_this_one = function (regExpLib, input) {
var result,
rGradientEnclosedInBrackets = /.*gradient\s*\(((?:\([^\)]*\)|[^\)\(]*)*)\)/,// Captures inside brackets - max one additional inner set.
match = rGradientEnclosedInBrackets.exec(input);
if (match !== null) {
// Get the parameters for the gradient
result = parseGradient(regExpLib, match[1]);
if (result.original.trim() !== match[1].trim()) {
// Did not match the input exactly - possible parsing error.
result.parseWarning = true;
}
} else {
result = "Failed to find gradient";
}
return result;
};
var test_this_thing = function () {
var result = [],
regExpLib = generateRegExp(),
testSubjects = [
// Original question sample
'background-image:linear-gradient(to right bottom, #FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%);',
// Sample to test RGBA values (1)
'background-image:linear-gradient(to right bottom, rgba(255, 0, 0, .1) 0%, rgba(0, 255, 0, 0.9) 20px);',
// Sample to test optional gradient line
'background-image:linear-gradient(#FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%);',
// Angle, named colors
'background: linear-gradient(45deg, red, blue);',
// Gradient that starts at 60% of the gradient line
'background: linear-gradient(135deg, orange, orange 60%, cyan);',
// Gradient with multi-position color stops
'background: linear-gradient(to right, red 20%, orange 20% 40%, yellow 40% 60%, green 60% 80%, blue 80%);'
];
for (var i = 0; i < testSubjects.length; i++) {
result.push(test_this_one(regExpLib, testSubjects[i]));
}
console.log(result);
};
test_this_thing();
Thanks for sharing some great time saving code. I noticed an error on the following line, which doesn't allow decimal opacity:
rDigits4 = /(\s*(?:[0-9]{1,3}\s*,\s*){3}[0-9]{1,3}\s*)/,// "(1, 2, 3, 4)"
I recommend using this instead, which also supports decimal values with more than 3 numbers:
rDigits4 = /(\s*(?:[0-9]{1,3}\s*,\s*){3}(?:[.\d]+)\s*)/,// "(1, 2, 3, .4)"
This parser from Rafael Caricio seems to work well, handling both linear and radial gradients.
Tested successful on the gradients listed below, most of which came from the wonderful solution from @DeanTaylor. The only problem with Dean's solution is the inability to handle radial gradients.
One gradient the parser chokes on is: radial-gradient(at 57% 50%, rgb(102, 126, 234) 0%, rgb(118, 75, 162) 100%)
Tested Gradients:
Code:
// Copyright (c) 2014 Rafael Caricio. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
var GradientParser = (GradientParser || {});
GradientParser.parse = (function() {
var tokens = {
linearGradient: /^(\-(webkit|o|ms|moz)\-)?(linear\-gradient)/i,
repeatingLinearGradient: /^(\-(webkit|o|ms|moz)\-)?(repeating\-linear\-gradient)/i,
radialGradient: /^(\-(webkit|o|ms|moz)\-)?(radial\-gradient)/i,
repeatingRadialGradient: /^(\-(webkit|o|ms|moz)\-)?(repeating\-radial\-gradient)/i,
sideOrCorner: /^to (left (top|bottom)|right (top|bottom)|left|right|top|bottom)/i,
extentKeywords: /^(closest\-side|closest\-corner|farthest\-side|farthest\-corner|contain|cover)/,
positionKeywords: /^(left|center|right|top|bottom)/i,
pixelValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))px/,
percentageValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))\%/,
emValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))em/,
angleValue: /^(-?(([0-9]*\.[0-9]+)|([0-9]+\.?)))deg/,
startCall: /^\(/,
endCall: /^\)/,
comma: /^,/,
hexColor: /^\#([0-9a-fA-F]+)/,
literalColor: /^([a-zA-Z]+)/,
rgbColor: /^rgb/i,
rgbaColor: /^rgba/i,
number: /^(([0-9]*\.[0-9]+)|([0-9]+\.?))/
};
var input = '';
function error(msg) {
var err = new Error(input + ': ' + msg);
err.source = input;
throw err;
}
function getAST() {
var ast = matchListDefinitions();
if (input.length > 0) {
error('Invalid input not EOF');
}
return ast;
}
function matchListDefinitions() {
return matchListing(matchDefinition);
}
function matchDefinition() {
return matchGradient(
'linear-gradient',
tokens.linearGradient,
matchLinearOrientation) ||
matchGradient(
'repeating-linear-gradient',
tokens.repeatingLinearGradient,
matchLinearOrientation) ||
matchGradient(
'radial-gradient',
tokens.radialGradient,
matchListRadialOrientations) ||
matchGradient(
'repeating-radial-gradient',
tokens.repeatingRadialGradient,
matchListRadialOrientations);
}
function matchGradient(gradientType, pattern, orientationMatcher) {
return matchCall(pattern, function(captures) {
var orientation = orientationMatcher();
if (orientation) {
if (!scan(tokens.comma)) {
error('Missing comma before color stops');
}
}
return {
type: gradientType,
orientation: orientation,
colorStops: matchListing(matchColorStop)
};
});
}
function matchCall(pattern, callback) {
var captures = scan(pattern);
if (captures) {
if (!scan(tokens.startCall)) {
error('Missing (');
}
result = callback(captures);
if (!scan(tokens.endCall)) {
error('Missing )');
}
return result;
}
}
function matchLinearOrientation() {
return matchSideOrCorner() ||
matchAngle();
}
function matchSideOrCorner() {
return match('directional', tokens.sideOrCorner, 1);
}
function matchAngle() {
return match('angular', tokens.angleValue, 1);
}
function matchListRadialOrientations() {
var radialOrientations,
radialOrientation = matchRadialOrientation(),
lookaheadCache;
if (radialOrientation) {
radialOrientations = [];
radialOrientations.push(radialOrientation);
lookaheadCache = input;
if (scan(tokens.comma)) {
radialOrientation = matchRadialOrientation();
if (radialOrientation) {
radialOrientations.push(radialOrientation);
} else {
input = lookaheadCache;
}
}
}
return radialOrientations;
}
function matchRadialOrientation() {
var radialType = matchCircle() ||
matchEllipse();
if (radialType) {
radialType.at = matchAtPosition();
} else {
var extent = matchExtentKeyword();
if (extent) {
radialType = extent;
var positionAt = matchAtPosition();
if (positionAt) {
radialType.at = positionAt;
}
} else {
var defaultPosition = matchPositioning();
if (defaultPosition) {
radialType = {
type: 'default-radial',
at: defaultPosition
};
}
}
}
return radialType;
}
function matchCircle() {
var circle = match('shape', /^(circle)/i, 0);
if (circle) {
circle.style = matchLength() || matchExtentKeyword();
}
return circle;
}
function matchEllipse() {
var ellipse = match('shape', /^(ellipse)/i, 0);
if (ellipse) {
ellipse.style = matchDistance() || matchExtentKeyword();
}
return ellipse;
}
function matchExtentKeyword() {
return match('extent-keyword', tokens.extentKeywords, 1);
}
function matchAtPosition() {
if (match('position', /^at/, 0)) {
var positioning = matchPositioning();
if (!positioning) {
error('Missing positioning value');
}
return positioning;
}
}
function matchPositioning() {
var location = matchCoordinates();
if (location.x || location.y) {
return {
type: 'position',
value: location
};
}
}
function matchCoordinates() {
return {
x: matchDistance(),
y: matchDistance()
};
}
function matchListing(matcher) {
var captures = matcher(),
result = [];
if (captures) {
result.push(captures);
while (scan(tokens.comma)) {
captures = matcher();
if (captures) {
result.push(captures);
} else {
error('One extra comma');
}
}
}
return result;
}
function matchColorStop() {
var color = matchColor();
if (!color) {
error('Expected color definition');
}
color.length = matchDistance();
return color;
}
function matchColor() {
return matchHexColor() ||
matchRGBAColor() ||
matchRGBColor() ||
matchLiteralColor();
}
function matchLiteralColor() {
return match('literal', tokens.literalColor, 0);
}
function matchHexColor() {
return match('hex', tokens.hexColor, 1);
}
function matchRGBColor() {
return matchCall(tokens.rgbColor, function() {
return {
type: 'rgb',
value: matchListing(matchNumber)
};
});
}
function matchRGBAColor() {
return matchCall(tokens.rgbaColor, function() {
return {
type: 'rgba',
value: matchListing(matchNumber)
};
});
}
function matchNumber() {
return scan(tokens.number)[1];
}
function matchDistance() {
return match('%', tokens.percentageValue, 1) ||
matchPositionKeyword() ||
matchLength();
}
function matchPositionKeyword() {
return match('position-keyword', tokens.positionKeywords, 1);
}
function matchLength() {
return match('px', tokens.pixelValue, 1) ||
match('em', tokens.emValue, 1);
}
function match(type, pattern, captureIndex) {
var captures = scan(pattern);
if (captures) {
return {
type: type,
value: captures[captureIndex]
};
}
}
function scan(regexp) {
var captures,
blankCaptures;
blankCaptures = /^[\n\r\t\s]+/.exec(input);
if (blankCaptures) {
consume(blankCaptures[0].length);
}
captures = regexp.exec(input);
if (captures) {
consume(captures[0].length);
}
return captures;
}
function consume(size) {
input = input.substr(size);
}
return function(code) {
input = code.toString();
return getAST();
};
})();