gradient-parser
Advanced tools
Comparing version
@@ -93,2 +93,6 @@ // Copyright (c) 2014 Rafael Caricio. All rights reserved. | ||
'visit_calc': function(node) { | ||
return 'calc(' + node.value + ')'; | ||
}, | ||
'visit_literal': function(node) { | ||
@@ -168,3 +172,3 @@ return visitor.visit_color(node.value, node); | ||
if (element instanceof Array) { | ||
return visitor.visit_array(element, result); | ||
return visitor.visit_array(element); | ||
} else if (typeof element === 'object' && !element.type) { | ||
@@ -220,2 +224,3 @@ return visitor.visit_object(element); | ||
varColor: /^var/i, | ||
calcValue: /^calc/i, | ||
variableName: /^(--[a-zA-Z0-9-,\s\#]+)/, | ||
@@ -308,4 +313,20 @@ number: /^(([0-9]*\.[0-9]+)|([0-9]+\.?))/, | ||
function matchLinearOrientation() { | ||
return matchSideOrCorner() || | ||
matchAngle(); | ||
// Check for standard CSS3 "to" direction | ||
var sideOrCorner = matchSideOrCorner(); | ||
if (sideOrCorner) { | ||
return sideOrCorner; | ||
} | ||
// Check for legacy single keyword direction (e.g., "right", "top") | ||
var legacyDirection = match('position-keyword', tokens.positionKeywords, 1); | ||
if (legacyDirection) { | ||
// For legacy syntax, we convert to the directional type | ||
return { | ||
type: 'directional', | ||
value: legacyDirection.value | ||
}; | ||
} | ||
// If neither, check for angle | ||
return matchAngle(); | ||
} | ||
@@ -360,8 +381,17 @@ | ||
} else { | ||
var defaultPosition = matchPositioning(); | ||
if (defaultPosition) { | ||
// Check for "at" position first, which is a common browser output format | ||
var atPosition = matchAtPosition(); | ||
if (atPosition) { | ||
radialType = { | ||
type: 'default-radial', | ||
at: defaultPosition | ||
at: atPosition | ||
}; | ||
} else { | ||
var defaultPosition = matchPositioning(); | ||
if (defaultPosition) { | ||
radialType = { | ||
type: 'default-radial', | ||
at: defaultPosition | ||
}; | ||
} | ||
} | ||
@@ -565,2 +595,3 @@ } | ||
matchPositionKeyword() || | ||
matchCalc() || | ||
matchLength(); | ||
@@ -573,2 +604,36 @@ } | ||
function matchCalc() { | ||
return matchCall(tokens.calcValue, function() { | ||
var openParenCount = 1; // Start with the opening parenthesis from calc( | ||
var i = 0; | ||
// Parse through the content looking for balanced parentheses | ||
while (openParenCount > 0 && i < input.length) { | ||
var char = input.charAt(i); | ||
if (char === '(') { | ||
openParenCount++; | ||
} else if (char === ')') { | ||
openParenCount--; | ||
} | ||
i++; | ||
} | ||
// If we exited because we ran out of input but still have open parentheses, error | ||
if (openParenCount > 0) { | ||
error('Missing closing parenthesis in calc() expression'); | ||
} | ||
// Get the content inside the calc() without the last closing paren | ||
var calcContent = input.substring(0, i - 1); | ||
// Consume the calc expression content | ||
consume(i - 1); // -1 because we don't want to consume the closing parenthesis | ||
return { | ||
type: 'calc', | ||
value: calcContent | ||
}; | ||
}); | ||
} | ||
function matchLength() { | ||
@@ -575,0 +640,0 @@ return match('px', tokens.pixelValue, 1) || |
@@ -32,2 +32,3 @@ var GradientParser = (window.GradientParser || {}); | ||
varColor: /^var/i, | ||
calcValue: /^calc/i, | ||
variableName: /^(--[a-zA-Z0-9-,\s\#]+)/, | ||
@@ -120,4 +121,20 @@ number: /^(([0-9]*\.[0-9]+)|([0-9]+\.?))/, | ||
function matchLinearOrientation() { | ||
return matchSideOrCorner() || | ||
matchAngle(); | ||
// Check for standard CSS3 "to" direction | ||
var sideOrCorner = matchSideOrCorner(); | ||
if (sideOrCorner) { | ||
return sideOrCorner; | ||
} | ||
// Check for legacy single keyword direction (e.g., "right", "top") | ||
var legacyDirection = match('position-keyword', tokens.positionKeywords, 1); | ||
if (legacyDirection) { | ||
// For legacy syntax, we convert to the directional type | ||
return { | ||
type: 'directional', | ||
value: legacyDirection.value | ||
}; | ||
} | ||
// If neither, check for angle | ||
return matchAngle(); | ||
} | ||
@@ -172,8 +189,17 @@ | ||
} else { | ||
var defaultPosition = matchPositioning(); | ||
if (defaultPosition) { | ||
// Check for "at" position first, which is a common browser output format | ||
var atPosition = matchAtPosition(); | ||
if (atPosition) { | ||
radialType = { | ||
type: 'default-radial', | ||
at: defaultPosition | ||
at: atPosition | ||
}; | ||
} else { | ||
var defaultPosition = matchPositioning(); | ||
if (defaultPosition) { | ||
radialType = { | ||
type: 'default-radial', | ||
at: defaultPosition | ||
}; | ||
} | ||
} | ||
@@ -377,2 +403,3 @@ } | ||
matchPositionKeyword() || | ||
matchCalc() || | ||
matchLength(); | ||
@@ -385,2 +412,36 @@ } | ||
function matchCalc() { | ||
return matchCall(tokens.calcValue, function() { | ||
var openParenCount = 1; // Start with the opening parenthesis from calc( | ||
var i = 0; | ||
// Parse through the content looking for balanced parentheses | ||
while (openParenCount > 0 && i < input.length) { | ||
var char = input.charAt(i); | ||
if (char === '(') { | ||
openParenCount++; | ||
} else if (char === ')') { | ||
openParenCount--; | ||
} | ||
i++; | ||
} | ||
// If we exited because we ran out of input but still have open parentheses, error | ||
if (openParenCount > 0) { | ||
error('Missing closing parenthesis in calc() expression'); | ||
} | ||
// Get the content inside the calc() without the last closing paren | ||
var calcContent = input.substring(0, i - 1); | ||
// Consume the calc expression content | ||
consume(i - 1); // -1 because we don't want to consume the closing parenthesis | ||
return { | ||
type: 'calc', | ||
value: calcContent | ||
}; | ||
}); | ||
} | ||
function matchLength() { | ||
@@ -524,2 +585,6 @@ return match('px', tokens.pixelValue, 1) || | ||
'visit_calc': function(node) { | ||
return 'calc(' + node.value + ')'; | ||
}, | ||
'visit_literal': function(node) { | ||
@@ -599,3 +664,3 @@ return visitor.visit_color(node.value, node); | ||
if (element instanceof Array) { | ||
return visitor.visit_array(element, result); | ||
return visitor.visit_array(element); | ||
} else if (typeof element === 'object' && !element.type) { | ||
@@ -602,0 +667,0 @@ return visitor.visit_object(element); |
@@ -30,2 +30,3 @@ // Copyright (c) 2014 Rafael Caricio. All rights reserved. | ||
varColor: /^var/i, | ||
calcValue: /^calc/i, | ||
variableName: /^(--[a-zA-Z0-9-,\s\#]+)/, | ||
@@ -118,4 +119,20 @@ number: /^(([0-9]*\.[0-9]+)|([0-9]+\.?))/, | ||
function matchLinearOrientation() { | ||
return matchSideOrCorner() || | ||
matchAngle(); | ||
// Check for standard CSS3 "to" direction | ||
var sideOrCorner = matchSideOrCorner(); | ||
if (sideOrCorner) { | ||
return sideOrCorner; | ||
} | ||
// Check for legacy single keyword direction (e.g., "right", "top") | ||
var legacyDirection = match('position-keyword', tokens.positionKeywords, 1); | ||
if (legacyDirection) { | ||
// For legacy syntax, we convert to the directional type | ||
return { | ||
type: 'directional', | ||
value: legacyDirection.value | ||
}; | ||
} | ||
// If neither, check for angle | ||
return matchAngle(); | ||
} | ||
@@ -170,8 +187,17 @@ | ||
} else { | ||
var defaultPosition = matchPositioning(); | ||
if (defaultPosition) { | ||
// Check for "at" position first, which is a common browser output format | ||
var atPosition = matchAtPosition(); | ||
if (atPosition) { | ||
radialType = { | ||
type: 'default-radial', | ||
at: defaultPosition | ||
at: atPosition | ||
}; | ||
} else { | ||
var defaultPosition = matchPositioning(); | ||
if (defaultPosition) { | ||
radialType = { | ||
type: 'default-radial', | ||
at: defaultPosition | ||
}; | ||
} | ||
} | ||
@@ -375,2 +401,3 @@ } | ||
matchPositionKeyword() || | ||
matchCalc() || | ||
matchLength(); | ||
@@ -383,2 +410,36 @@ } | ||
function matchCalc() { | ||
return matchCall(tokens.calcValue, function() { | ||
var openParenCount = 1; // Start with the opening parenthesis from calc( | ||
var i = 0; | ||
// Parse through the content looking for balanced parentheses | ||
while (openParenCount > 0 && i < input.length) { | ||
var char = input.charAt(i); | ||
if (char === '(') { | ||
openParenCount++; | ||
} else if (char === ')') { | ||
openParenCount--; | ||
} | ||
i++; | ||
} | ||
// If we exited because we ran out of input but still have open parentheses, error | ||
if (openParenCount > 0) { | ||
error('Missing closing parenthesis in calc() expression'); | ||
} | ||
// Get the content inside the calc() without the last closing paren | ||
var calcContent = input.substring(0, i - 1); | ||
// Consume the calc expression content | ||
consume(i - 1); // -1 because we don't want to consume the closing parenthesis | ||
return { | ||
type: 'calc', | ||
value: calcContent | ||
}; | ||
}); | ||
} | ||
function matchLength() { | ||
@@ -385,0 +446,0 @@ return match('px', tokens.pixelValue, 1) || |
@@ -93,2 +93,6 @@ // Copyright (c) 2014 Rafael Caricio. All rights reserved. | ||
'visit_calc': function(node) { | ||
return 'calc(' + node.value + ')'; | ||
}, | ||
'visit_literal': function(node) { | ||
@@ -168,3 +172,3 @@ return visitor.visit_color(node.value, node); | ||
if (element instanceof Array) { | ||
return visitor.visit_array(element, result); | ||
return visitor.visit_array(element); | ||
} else if (typeof element === 'object' && !element.type) { | ||
@@ -171,0 +175,0 @@ return visitor.visit_object(element); |
{ | ||
"name": "gradient-parser", | ||
"version": "1.1.0", | ||
"version": "1.1.1", | ||
"description": "Parse CSS3 gradient definitions and return an AST.", | ||
@@ -43,3 +43,3 @@ "author": { | ||
"expect.js": "^0.3.1", | ||
"esbuild": "^0.20.2", | ||
"esbuild": "^0.25.0", | ||
"mocha": "^10.3.0" | ||
@@ -46,0 +46,0 @@ }, |
@@ -142,3 +142,5 @@ 'use strict'; | ||
{type: 'directional', unparsedValue: 'to bottom left', value: 'bottom left'}, | ||
{type: 'directional', unparsedValue: 'to bottom right', value: 'bottom right'} | ||
{type: 'directional', unparsedValue: 'to bottom right', value: 'bottom right'}, | ||
{type: 'directional', unparsedValue: 'to bottom', value: 'bottom'}, // Test modern syntax | ||
{type: 'directional', unparsedValue: 'bottom', value: 'bottom'} // Test legacy syntax | ||
].forEach(function(orientation) { | ||
@@ -157,2 +159,86 @@ describe('parse orientation ' + orientation.type, function() { | ||
}); | ||
it('should correctly parse directional value without "to" keyword (legacy syntax)', function() { | ||
// This uses the legacy syntax without "to" keyword (e.g., "right" instead of "to right") | ||
const parsed = gradients.parse('-webkit-linear-gradient(right, rgb(248, 6, 234) 71%, rgb(202, 74, 208) 78%)'); | ||
let subject = parsed[0]; | ||
// It should properly identify the orientation as directional "right" | ||
expect(subject.orientation).to.be.an('object'); | ||
expect(subject.orientation.type).to.equal('directional'); | ||
expect(subject.orientation.value).to.equal('right'); | ||
// And it should have only 2 color stops | ||
expect(subject.colorStops).to.have.length(2); | ||
expect(subject.colorStops[0].type).to.equal('rgb'); | ||
expect(subject.colorStops[0].value).to.eql(['248', '6', '234']); | ||
expect(subject.colorStops[0].length.type).to.equal('%'); | ||
expect(subject.colorStops[0].length.value).to.equal('71'); | ||
expect(subject.colorStops[1].type).to.equal('rgb'); | ||
expect(subject.colorStops[1].value).to.eql(['202', '74', '208']); | ||
expect(subject.colorStops[1].length.type).to.equal('%'); | ||
expect(subject.colorStops[1].length.value).to.equal('78'); | ||
}); | ||
// Additional test cases for other legacy directional keywords | ||
it('should correctly parse legacy syntax with "top" direction', function() { | ||
const parsed = gradients.parse('-webkit-linear-gradient(top, #ff0000, #0000ff)'); | ||
let subject = parsed[0]; | ||
expect(subject.orientation).to.be.an('object'); | ||
expect(subject.orientation.type).to.equal('directional'); | ||
expect(subject.orientation.value).to.equal('top'); | ||
expect(subject.colorStops).to.have.length(2); | ||
expect(subject.colorStops[0].type).to.equal('hex'); | ||
expect(subject.colorStops[0].value).to.equal('ff0000'); | ||
expect(subject.colorStops[1].type).to.equal('hex'); | ||
expect(subject.colorStops[1].value).to.equal('0000ff'); | ||
}); | ||
it('should correctly parse "to bottom" direction (modern syntax)', function() { | ||
const parsed = gradients.parse('linear-gradient(to bottom, rgb(0, 91, 154), rgb(230, 193, 61))'); | ||
let subject = parsed[0]; | ||
expect(subject.orientation).to.be.an('object'); | ||
expect(subject.orientation.type).to.equal('directional'); | ||
expect(subject.orientation.value).to.equal('bottom'); | ||
expect(subject.colorStops).to.have.length(2); | ||
expect(subject.colorStops[0].type).to.equal('rgb'); | ||
expect(subject.colorStops[0].value).to.eql(['0', '91', '154']); | ||
expect(subject.colorStops[1].type).to.equal('rgb'); | ||
expect(subject.colorStops[1].value).to.eql(['230', '193', '61']); | ||
}); | ||
it('should correctly parse legacy syntax with "left" direction', function() { | ||
const parsed = gradients.parse('-webkit-linear-gradient(left, rgba(255, 0, 0, 0.5), rgba(0, 0, 255, 0.8))'); | ||
let subject = parsed[0]; | ||
expect(subject.orientation).to.be.an('object'); | ||
expect(subject.orientation.type).to.equal('directional'); | ||
expect(subject.orientation.value).to.equal('left'); | ||
expect(subject.colorStops).to.have.length(2); | ||
expect(subject.colorStops[0].type).to.equal('rgba'); | ||
expect(subject.colorStops[0].value).to.eql(['255', '0', '0', '0.5']); | ||
expect(subject.colorStops[1].type).to.equal('rgba'); | ||
expect(subject.colorStops[1].value).to.eql(['0', '0', '255', '0.8']); | ||
}); | ||
it('should correctly parse legacy syntax with "bottom" direction', function() { | ||
const parsed = gradients.parse('-webkit-linear-gradient(bottom, hsla(0, 100%, 50%, 0.3), hsla(240, 100%, 50%, 0.7))'); | ||
let subject = parsed[0]; | ||
expect(subject.orientation).to.be.an('object'); | ||
expect(subject.orientation.type).to.equal('directional'); | ||
expect(subject.orientation.value).to.equal('bottom'); | ||
expect(subject.colorStops).to.have.length(2); | ||
expect(subject.colorStops[0].type).to.equal('hsla'); | ||
expect(subject.colorStops[0].value).to.eql(['0', '100', '50', '0.3']); | ||
expect(subject.colorStops[1].type).to.equal('hsla'); | ||
expect(subject.colorStops[1].value).to.eql(['240', '100', '50', '0.7']); | ||
}); | ||
}); | ||
@@ -302,2 +388,29 @@ | ||
}); | ||
it('should parse radial-gradient with position only (no shape/extent)', function() { | ||
const gradient = 'radial-gradient(at 57% 50%, rgb(102, 126, 234) 0%, rgb(118, 75, 162) 100%)'; | ||
const ast = gradients.parse(gradient); | ||
expect(ast[0].type).to.equal('radial-gradient'); | ||
// Verify the orientation (position only) | ||
expect(ast[0].orientation[0].type).to.equal('default-radial'); | ||
expect(ast[0].orientation[0].at.type).to.equal('position'); | ||
expect(ast[0].orientation[0].at.value.x.type).to.equal('%'); | ||
expect(ast[0].orientation[0].at.value.x.value).to.equal('57'); | ||
expect(ast[0].orientation[0].at.value.y.type).to.equal('%'); | ||
expect(ast[0].orientation[0].at.value.y.value).to.equal('50'); | ||
// Verify color stops | ||
expect(ast[0].colorStops).to.have.length(2); | ||
expect(ast[0].colorStops[0].type).to.equal('rgb'); | ||
expect(ast[0].colorStops[0].value).to.eql(['102', '126', '234']); | ||
expect(ast[0].colorStops[0].length.type).to.equal('%'); | ||
expect(ast[0].colorStops[0].length.value).to.equal('0'); | ||
expect(ast[0].colorStops[1].type).to.equal('rgb'); | ||
expect(ast[0].colorStops[1].value).to.eql(['118', '75', '162']); | ||
expect(ast[0].colorStops[1].length.type).to.equal('%'); | ||
expect(ast[0].colorStops[1].length.value).to.equal('100'); | ||
}); | ||
}); | ||
@@ -382,4 +495,120 @@ | ||
}); | ||
describe('parse calc expressions', function() { | ||
it('should parse linear gradient with calc in color stop position', function() { | ||
const gradient = 'linear-gradient(to right, red calc(10% + 20px), blue 50%)'; | ||
const ast = gradients.parse(gradient); | ||
expect(ast[0].type).to.equal('linear-gradient'); | ||
expect(ast[0].orientation.type).to.equal('directional'); | ||
expect(ast[0].orientation.value).to.equal('right'); | ||
expect(ast[0].colorStops).to.have.length(2); | ||
expect(ast[0].colorStops[0].type).to.equal('literal'); | ||
expect(ast[0].colorStops[0].value).to.equal('red'); | ||
expect(ast[0].colorStops[0].length.type).to.equal('calc'); | ||
expect(ast[0].colorStops[0].length.value).to.equal('10% + 20px'); | ||
expect(ast[0].colorStops[1].type).to.equal('literal'); | ||
expect(ast[0].colorStops[1].value).to.equal('blue'); | ||
expect(ast[0].colorStops[1].length.type).to.equal('%'); | ||
expect(ast[0].colorStops[1].length.value).to.equal('50'); | ||
}); | ||
it('should parse radial gradient with calc in position', function() { | ||
const gradient = 'radial-gradient(circle at calc(50% + 25px) 50%, red, blue)'; | ||
const ast = gradients.parse(gradient); | ||
expect(ast[0].type).to.equal('radial-gradient'); | ||
expect(ast[0].orientation[0].type).to.equal('shape'); | ||
expect(ast[0].orientation[0].value).to.equal('circle'); | ||
// Check the position | ||
expect(ast[0].orientation[0].at.type).to.equal('position'); | ||
expect(ast[0].orientation[0].at.value.x.type).to.equal('calc'); | ||
expect(ast[0].orientation[0].at.value.x.value).to.equal('50% + 25px'); | ||
expect(ast[0].orientation[0].at.value.y.type).to.equal('%'); | ||
expect(ast[0].orientation[0].at.value.y.value).to.equal('50'); | ||
// Check the color stops | ||
expect(ast[0].colorStops).to.have.length(2); | ||
expect(ast[0].colorStops[0].value).to.equal('red'); | ||
expect(ast[0].colorStops[1].value).to.equal('blue'); | ||
}); | ||
it('should parse calc expressions with multiple operations', function() { | ||
const gradient = 'linear-gradient(90deg, yellow calc(100% - 50px), green calc(100% - 20px))'; | ||
const ast = gradients.parse(gradient); | ||
expect(ast[0].type).to.equal('linear-gradient'); | ||
expect(ast[0].orientation.type).to.equal('angular'); | ||
expect(ast[0].orientation.value).to.equal('90'); | ||
expect(ast[0].colorStops).to.have.length(2); | ||
expect(ast[0].colorStops[0].type).to.equal('literal'); | ||
expect(ast[0].colorStops[0].value).to.equal('yellow'); | ||
expect(ast[0].colorStops[0].length.type).to.equal('calc'); | ||
expect(ast[0].colorStops[0].length.value).to.equal('100% - 50px'); | ||
expect(ast[0].colorStops[1].type).to.equal('literal'); | ||
expect(ast[0].colorStops[1].value).to.equal('green'); | ||
expect(ast[0].colorStops[1].length.type).to.equal('calc'); | ||
expect(ast[0].colorStops[1].length.value).to.equal('100% - 20px'); | ||
}); | ||
it('should parse calc expressions with nested parentheses', function() { | ||
const gradient = 'linear-gradient(to bottom, red calc(50% + (25px * 2)), blue)'; | ||
const ast = gradients.parse(gradient); | ||
expect(ast[0].type).to.equal('linear-gradient'); | ||
expect(ast[0].orientation.type).to.equal('directional'); | ||
expect(ast[0].orientation.value).to.equal('bottom'); | ||
expect(ast[0].colorStops).to.have.length(2); | ||
expect(ast[0].colorStops[0].type).to.equal('literal'); | ||
expect(ast[0].colorStops[0].value).to.equal('red'); | ||
expect(ast[0].colorStops[0].length.type).to.equal('calc'); | ||
expect(ast[0].colorStops[0].length.value).to.equal('50% + (25px * 2)'); | ||
}); | ||
it('should parse multiple calc expressions in the same gradient', function() { | ||
const gradient = 'radial-gradient(circle at calc(50% - 10px) calc(50% + 10px), red calc(20% + 10px), blue)'; | ||
const ast = gradients.parse(gradient); | ||
expect(ast[0].type).to.equal('radial-gradient'); | ||
expect(ast[0].orientation[0].type).to.equal('shape'); | ||
expect(ast[0].orientation[0].value).to.equal('circle'); | ||
// Check the position | ||
expect(ast[0].orientation[0].at.type).to.equal('position'); | ||
expect(ast[0].orientation[0].at.value.x.type).to.equal('calc'); | ||
expect(ast[0].orientation[0].at.value.x.value).to.equal('50% - 10px'); | ||
expect(ast[0].orientation[0].at.value.y.type).to.equal('calc'); | ||
expect(ast[0].orientation[0].at.value.y.value).to.equal('50% + 10px'); | ||
// Check the color stops | ||
expect(ast[0].colorStops).to.have.length(2); | ||
expect(ast[0].colorStops[0].type).to.equal('literal'); | ||
expect(ast[0].colorStops[0].value).to.equal('red'); | ||
expect(ast[0].colorStops[0].length.type).to.equal('calc'); | ||
expect(ast[0].colorStops[0].length.value).to.equal('20% + 10px'); | ||
}); | ||
it('should throw an error for unbalanced parentheses in calc expressions', function() { | ||
// Different test cases throw different errors, so we need to be more specific | ||
expect(function() { | ||
gradients.parse('linear-gradient(to right, red calc(50% + (25px), blue)'); | ||
}).to.throwException(); | ||
expect(function() { | ||
gradients.parse('radial-gradient(circle at calc(50% + 25px, red, blue)'); | ||
}).to.throwException(/Missing comma before color stops/); | ||
expect(function() { | ||
gradients.parse('linear-gradient(90deg, yellow calc(100% - (50px - 20px), green)'); | ||
}).to.throwException(); | ||
}); | ||
}); | ||
}); | ||
}); |
@@ -18,3 +18,16 @@ 'use strict'; | ||
describe('serialization', function() { | ||
it('should handle array input without error', function() { | ||
const nodes = [{ | ||
type: 'linear-gradient', | ||
colorStops: [{ type: 'literal', value: 'red' }, { type: 'literal', value: 'blue' }], | ||
orientation: null | ||
}]; | ||
expect(function() { | ||
const result = gradients.stringify(nodes); | ||
expect(result).to.equal('linear-gradient(red, blue)'); | ||
}).to.not.throwException(); | ||
}); | ||
it('if tree is null', function() { | ||
@@ -21,0 +34,0 @@ expect(gradients.stringify(null)).to.equal(''); |
92246
21.95%2340
18.48%15
-16.67%