【问题标题】:parse css gradient rule with Javascript regex使用 Javascript 正则表达式解析 CSS 梯度规则
【发布时间】:2013-11-26 11:09:47
【问题描述】:

在我的 css 文件中,我有渐变规则,如下所示:

background-image:linear-gradient(to right, #FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%);

我想得到这个字符串的所有部分。预期输出为:

linear-gradient
to right
#FF0000 
0%, 
#00FF00 
20px, 
rgb(0, 0, 255) 
100%

处理整个字符串对我来说太难了,所以我决定把它分成几部分。

线性渐变

.*gradient[^\(]?

颜色

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]?)?

向右

(?<=\()(.*(top|left|right|bottom|center).*?)(?=,)

但最后一个正则表达式在 JS 中不起作用,因为它没有正则表达式后视。 很快,我需要在“(”和“,”之间获取所有内容

【问题讨论】:

    标签: javascript css regex gradient


    【解决方案1】:

    解析 CSS 可能要复杂得多,需要记住的事情很少:

    • 避免编写解析器 - 其他人可能已经编写了解析器(搜索)。
    • 如果您不控制输入源或不使用输入样本彻底测试,您的解析器可能会失败。
    • 在渐变的情况下,您可以有“角度”以及“角边”,如“右”。
    • 存在未知数量的色标(最少 1 个)。
    • 您绝不可能希望在正则表达式中包含完整的 CSS 颜色列表(例如 redblue 等)。
    • 您应该查看MDN 了解语法变化的详细信息,下面的示例代码仅支持标准语法。
    • 正则表达式支持和错误因浏览器和版本而异 - 使用所有示例测试您的目标浏览器。

    好的,这里有一个疯狂的例子,说明你如何“可以”使用正则表达式解析渐变 - 我并不是说你应该这样做。

    在这里,我在代码中构建了我的正则表达式,以保持代码的某种程度的可读性和可维护性。

    test_this_thing函数console.log(result);的最终输出如下:

    输入:

    background-image:linear-gradient(to right bottom, #FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%);

    输出:

    {
       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%"
          }
       ]
    }
    

    请注意,输出包含original 属性——这看起来像输入——但如果部分输入不匹配,inputoriginal 的值会有所不同;注意解析器中可能存在的错误。

    这是一个来源:

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

    【讨论】:

    • 进行了小更新以添加按名称支持的颜色,例如“红色”“透明”、“黑色”等
    • 顺便说一句:在 C# 中使用 Alba.CsCss 解析器非常简单。
    • 有人遇到过现有的 js 解析器吗?我一直找不到。感谢您发布此@DeanTaylor
    • 已更新以修复 RGBA,添加了对停止长度的支持,可选渐变线,添加了更多测试样本,简化了一些正则表达式,添加了用于测试的 parseWarning 指标。哦,并且在正则表达式上重置了示例代码lastIndex,作为如何重用它们的示例,避免重新创建它们。
    • 谢谢@DeanTaylor!万一有人需要,我把它变成了 TypeScript:gist.github.com/waterplea/6cde85e4655202dccf1ee31b5ab3c5fb 去掉了 ifs,因为在大多数情况下,未定义或丢失的属性最终都无关紧要。如果您使用它并觉得需要它们,请随时将它们重新添加。
    【解决方案2】:

    感谢您分享一些节省时间的好代码。我注意到以下行出现错误,不允许小数不透明度:

    rDigits4 = /(\s*(?:[0-9]{1,3}\s*,\s*){3}[0-9]{1,3}\s*)/,// "(1, 2, 3, 4)"

    我建议改用它,它也支持超过 3 个数字的十进制值:

    rDigits4 = /(\s*(?:[0-9]{1,3}\s*,\s*){3}(?:[.\d]+)\s*)/,// "(1, 2, 3, .4)"

    【讨论】:

      【解决方案3】:

      Rafael Caricio 的这个解析器似乎运行良好,可以处理线性和径向渐变。

      在下面列出的渐变上测试成功,其中大部分来自@DeanTaylor 的精彩解决方案。 Dean 解决方案的唯一问题是无法处理径向渐变。

      解析器阻塞的一个梯度是:radial-gradient(at 57% 50%, rgb(102, 126, 234) 0%, rgb(118, 75, 162) 100%)

      测试梯度:

      • 线性渐变(到右下角,#FF0000 0%,#00FF00 20px,rgb(0, 0, 255) 100%)
      • linear-gradient(到右下角, rgba(255, 0, 0, .1) 0%, rgba(0, 255, 0, 0.9) 20px)
      • 径向渐变(rgb(102, 126, 234), rgb(118, 75, 162))
      • 线性渐变(#FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%)
      • 线性渐变(45度,红,蓝)
      • 线性渐变(135deg,橙色,橙色 60%,青色)
      • 线性渐变(向右,红色 20%,橙色 20% 40%,黄色 40% 60%,绿色 60% 80%,蓝色 80%)
      • 径向渐变(rgb(102, 126, 234), rgb(118, 75, 162))
      • 径向渐变(100% 处的圆,#333,#333 50%,#eee 75%,#333 75%)
      • 径向渐变(椭圆最远边为 16% 35%,#ff0000 0%,#00ff00 80%)
      • radial-gradient(圆最远边为 28% 50%, #ff0000 0%, #00ff00 80%)
      • radial-gradient(圆最远角,28% 50%,#ff0000 0%,#00ff00 80%)

      代码:

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 2010-12-09
        • 2015-05-30
        • 1970-01-01
        • 1970-01-01
        • 2010-09-19
        • 2011-10-09
        • 2011-05-24
        • 1970-01-01
        相关资源
        最近更新 更多