Разобрать правило градиента CSS с помощью регулярных выражений Javascript

В моем 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, потому что оно не имеет обратного выражения. Короче, мне нужно здесь, чтобы получить все между "(" и ","

2 ответа

Решение

Синтаксический анализ CSS может быть гораздо более сложным, есть несколько вещей, которые нужно запомнить:

  • Избегайте написания парсера - возможно, кто-то уже написал его (поиск).
  • Ваш синтаксический анализатор, скорее всего, потерпит неудачу, если вы не контролируете источник входного сигнала или не проверяете его с помощью входных выборок.
  • В случае градиентов вы можете иметь как "углы", так и "угловые стороны", например "вправо".
  • Есть неизвестное количество цветовых остановок (минимум 1).
  • Вы никогда не захотите включать полный список цветов CSS в регулярное выражение (например, red, blue, так далее).
  • Вы должны проверить 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 свойство - это похоже на вход - но если часть ввода не соответствует input а также original значения будут другими; отмечая возможную ошибку в парсере.

Вот источник:

    /**
     * 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-Fa-f0-9]{6}|[A-Fa-f0-9]{3})/, // 3 or 6 character form
            rDigits3 = /\(\s*(?:[0-9]{1,3}\s*,\s*){2}[0-9]{1,3}\s*\)/,// "(1, 2, 3)"
            rDigits4 = /\(\s*(?:[0-9]{1,3}\s*,\s*){3}[0-9]{1,3}\s*\)/,// "(1, 2, 3, 4)"
            rValue = /(?:[+-]?\d*\.?\d+)(?:%|[a-z]+)?/,// ".9", "-5px", "100%".
            rKeyword = /[_A-Za-z-][_A-Za-z0-9-]*/,// "red", "transparent", "border-collapse".
            rColor = combineRegExp([
                '(?:', rColorHex, '|', '(?:rgb|hsl)', rDigits3, '|', '(?:rgba|hsla)', rDigits4, '|', rKeyword, ')'
            ], ''),
            rColorStop = combineRegExp([rColor, '(?:\\s+', rValue, ')?'], ''),// Single Color Stop, optional value.
            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;

        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];
            }

            // 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_thing = function () {

        var result,
            regExpLib = generateRegExp(),
            input = 'background-image:linear-gradient(to right bottom, #FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%);',
            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]);
        } else {
            result = "Failed to find gradient";
        }

        console.log(result);
    };
    test_this_thing();

Спасибо за то, что поделились с нами отличным кодом для экономии времени. Я заметил ошибку в следующей строке, которая не позволяет использовать десятичную непрозрачность:

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) "

Этот парсер от Рафаэля Карисио, кажется, работает хорошо, обрабатывая как линейные, так и радиальные градиенты.

Успешно протестирован на градиентах, перечисленных ниже, большинство из которых было получено благодаря замечательному решению от @DeanTaylor. Единственная проблема с решением Дина - невозможность обрабатывать радиальные градиенты.

Один градиент, на который подавляется парсер: radial-gradient(at 57% 50%, rgb(102, 126, 234) 0%, rgb(118, 75, 162) 100%)

Протестированные градиенты:

  • linear-gradient (справа внизу, # FF0000 0%, # 00FF00 20px, rgb (0, 0, 255) 100%)
  • linear-gradient (справа внизу, rgba(255, 0, 0, .1) 0%, rgba(0, 255, 0, 0.9) 20 пикселей)
  • радиальный градиент (rgb(102, 126, 234), rgb(118, 75, 162))
  • линейный градиент (#FF0000 0%, #00FF00 20px, rgb(0, 0, 255) 100%)
  • линейный градиент (45 градусов, красный, синий)
  • линейный градиент (135 градусов, оранжевый, оранжевый 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%)
  • радиальный градиент (крайняя сторона круга на 28% 50%, #ff0000 0%, #00ff00 80%)
  • радиальный градиент (самый дальний угол круга на 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();
  };
})();
Другие вопросы по тегам