Разобрать правило градиента 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();
};
})();