parse css gradient rule with Javascript regex

css, gradient, javascript, regex

Solution

Parsing CSS can be far more complex there are few things to remember:

- Avoid writing a parser - someone else will probably have written one already (search).

- Your parser will likely fail if you don't control the input source or test it thoroughly with input samples.

- In the case of gradients you can have "angles" as well as "corner-sides" like "right".

- There are an unknown number of color stops (minimum 1).

- You will never likely want to include the complete list of CSS colors in a regular expression (e.g. `red`, `blue`, etc).

- You should check out MDN for details of syntax variations, the sample code below only supports the standard syntax.

- Regular expression support and bugs are different depending on the browser and version - test your target browsers with all your samples.

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();

Problem

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%); ``` I want to get all parts of this string. Expected output is: ``` linear-gradient to right #FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100% ``` it was too difficult for me to work on whole string so I decided to split it into parts. linear-gradient ``` .*gradient[^\(]? ``` colors ``` rgb ?\([ 0-9.%,]+?\)|#[0-9a-fA-F]{3,6}\s[0-9]{1,3}[%|px]|#[0-9a-fA-F]{3,6}|(aqua|black|blue|fuchsia|gray|green|lime|maroon|navy|olive|orange|purple|red|silver|teal|white|yellow){1}(\s[0-9]{1,3}\s*[%|px]?)? ``` to right ``` (?<=\()(.*(top|left|right|bottom|center).*?)(?=,) ``` but the last regex not works in JS because it doesn't have regex lookbehind. Shortly, I need here to get all between "(" and ","

Original source