🚀 Big News: Socket Acquires Coana to Bring Reachability Analysis to Every Appsec Team.Learn more →

gradient-parser

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

gradient-parser - npm Package Compare versions

Comparing version

to
1.1.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) {

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