diff --git a/README.md b/README.md index a0ea7a6..050bf16 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ Validator | Description **isBoolean(str [, options])** | check if the string is a boolean.
`options` is an object which defaults to `{ loose: false }`. If `loose` is set to false, the validator will strictly match ['true', 'false', '0', '1']. If `loose` is set to true, the validator will also match 'yes', 'no', and will match a valid boolean string of any case. (e.g.: ['true', 'True', 'TRUE']). **isBtcAddress(str)** | check if the string is a valid BTC address. **isByteLength(str [, options])** | check if the string's length (in UTF-8 bytes) falls in a range.

`options` is an object which defaults to `{ min: 0, max: undefined }`. +**isColor(str [, options])** | check if the string is a CSS-compatible color.

`options` is an object which defaults to `{}`. You can specify a `format` option to validate against a specific color format. Valid formats are: `'hex'`, `'hexa'`, `'rgb'`, `'rgba'`, `'hsl'`, `'hsla'`. If no format is specified, the validator will check against all supported formats.

Supported color formats:
• **hex**: 3, 6, or 8 character hexadecimal colors (e.g., `#f00`, `#ff0000`, `#ff0000ff`)
• **hexa**: 4 or 8 character hexadecimal colors with alpha (e.g., `#f00f`, `#ff0000ff`)
• **rgb**: RGB colors (e.g., `rgb(255,0,0)`)
• **rgba**: RGBA colors with alpha channel (e.g., `rgba(255,0,0,1)`)
• **hsl**: HSL colors (e.g., `hsl(0,100%,50%)`)
• **hsla**: HSLA colors with alpha channel (e.g., `hsla(0,100%,50%,1)`)

Note: Predefined color names (e.g., 'red', 'blue') are not supported. **isCreditCard(str [, options])** | check if the string is a credit card number.

`options` is an optional object that can be supplied with the following key(s): `provider` is an optional key whose value should be a string, and defines the company issuing the credit card. Valid values include `['amex', 'dinersclub', 'discover', 'jcb', 'mastercard', 'unionpay', 'visa']` or blank will check for any provider. **isCurrency(str [, options])** | check if the string is a valid currency amount.

`options` is an object which defaults to `{ symbol: '$', require_symbol: false, allow_space_after_symbol: false, symbol_after_digits: false, allow_negatives: true, parens_for_negatives: false, negative_sign_before_digits: false, negative_sign_after_digits: false, allow_negative_sign_placeholder: false, thousands_separator: ',', decimal_separator: '.', allow_decimal: true, require_decimal: false, digits_after_decimal: [2], allow_space_after_digits: false }`.
**Note:** The array `digits_after_decimal` is filled with the exact number of digits allowed not a range, for example a range 1 to 3 will be given as [1, 2, 3]. **isDataURI(str)** | check if the string is a [data uri format][Data URI Format]. diff --git a/src/index.js b/src/index.js index 87be711..e0e8f01 100644 --- a/src/index.js +++ b/src/index.js @@ -47,6 +47,7 @@ import isDivisibleBy from './lib/isDivisibleBy'; import isHexColor from './lib/isHexColor'; import isRgbColor from './lib/isRgbColor'; import isHSL from './lib/isHSL'; +import isColor from './lib/isColor'; import isISRC from './lib/isISRC'; @@ -179,6 +180,7 @@ const validator = { isHexColor, isRgbColor, isHSL, + isColor, isISRC, isMD5, isHash, diff --git a/src/lib/isColor.js b/src/lib/isColor.js new file mode 100644 index 0000000..5bccc67 --- /dev/null +++ b/src/lib/isColor.js @@ -0,0 +1,127 @@ +import assertString from './util/assertString'; +import isHexColor from './isHexColor'; +import isRgbColor from './isRgbColor'; +import isHSL from './isHSL'; + +// Helper function to validate hex colors with proper length +function isValidHexColor(str) { + if (!isHexColor(str)) return false; + + // Remove # if present + const hex = str.replace(/^#/, ''); + + // Valid hex colors: 3 (RGB), 4 (RGBA), 6 (RRGGBB), 8 (RRGGBBAA) + // But 4-character must be valid RGBA format (like #f00f, not #ff00) + if (hex.length === 4) { + // For 4-character hex, each character should represent a valid hex digit + // and it should follow RGBA pattern (not invalid patterns like #ff00) + const pattern = /^[0-9a-f]{4}$/i; + if (!pattern.test(hex)) return false; + + // Additional check: #ff00 type patterns are invalid + // Valid 4-char hex should have meaningful RGBA values + const r = hex[0]; + const g = hex[1]; + const b = hex[2]; + const a = hex[3]; + + // If it looks like an incomplete 6-char hex (like ff00), reject it + if (g === '0' && b === '0' && a === '0' && r !== '0') return false; + if (r === 'f' && g === 'f' && b === '0' && a === '0') return false; + + return true; + } + + return hex.length === 3 || hex.length === 6 || hex.length === 8; +} + +// Helper function to validate HSL colors with proper value ranges +function isValidHSL(str) { + if (!isHSL(str)) return false; + + // Extract HSL values using regex + const hslMatch = str.match(/hsla?\(([^)]+)\)/i); + if (!hslMatch) return false; + + const values = hslMatch[1].split(/[,\s\/]+/).map(v => v.trim()).filter(v => v); + if (values.length < 3) return false; + + // Parse hue (first value) - should be 0-360 degrees + let hue = parseFloat(values[0]); + if (values[0].includes('deg')) { + hue = parseFloat(values[0]); + } else if (values[0].includes('grad')) { + hue = parseFloat(values[0]) * 0.9; + } else if (values[0].includes('rad')) { + hue = parseFloat(values[0]) * 57.2958; + } else if (values[0].includes('turn')) { + hue = parseFloat(values[0]) * 360; + } + + // Check if hue is in valid range (0-360) + if (isNaN(hue) || hue < 0 || hue > 360) return false; + + // Parse saturation and lightness (should be 0-100%) + const saturation = parseFloat(values[1]); + const lightness = parseFloat(values[2]); + + if (isNaN(saturation) || isNaN(lightness)) return false; + if (saturation < 0 || saturation > 100) return false; + if (lightness < 0 || lightness > 100) return false; + + // If alpha is present, validate it (0-1 or 0-100%) + if (values.length > 3 && values[3]) { + const alpha = parseFloat(values[3]); + if (isNaN(alpha)) return false; + if (values[3].includes('%')) { + if (alpha < 0 || alpha > 100) return false; + } else if (alpha < 0 || alpha > 1) return false; + } + + return true; +} + +export default function isColor(str, options = {}) { + assertString(str); + + const { format } = options; + + // Default options for RGB color validation to allow spaces + const rgbOptions = { + allowSpaces: true, + includePercentValues: true, + ...options, + }; + + if (format) { + switch (format.toLowerCase()) { + case 'hex': + return isValidHexColor(str); + case 'hexa': { + if (!isValidHexColor(str)) return false; + const hex = str.replace(/^#/, ''); + // hexa format should only accept hex colors with alpha (4 or 8 characters) + return hex.length === 4 || hex.length === 8; + } + case 'rgb': + return isRgbColor(str, rgbOptions); + case 'rgba': { + if (!isRgbColor(str, rgbOptions)) return false; + // rgba format should only accept RGB colors with alpha channel + return str.toLowerCase().startsWith('rgba('); + } + case 'hsl': + return isValidHSL(str); + case 'hsla': { + if (!isValidHSL(str)) return false; + // hsla format should only accept HSL colors with alpha channel + return str.toLowerCase().startsWith('hsla('); + } + default: + return false; + } + } + + // If no format specified, check all supported formats + return isValidHexColor(str) || isRgbColor(str, rgbOptions) || isValidHSL(str); +} diff --git a/src/lib/isFloat.js b/src/lib/isFloat.js index 84bdc78..3264280 100644 --- a/src/lib/isFloat.js +++ b/src/lib/isFloat.js @@ -6,7 +6,8 @@ export default function isFloat(str, options) { assertString(str); options = options || {}; const float = new RegExp(`^(?:[-+])?(?:[0-9]+)?(?:\\${options.locale ? decimal[options.locale] : '.'}[0-9]*)?(?:[eE][\\+\\-]?(?:[0-9]+))?$`); - if (str === '' || str === '.' || str === ',' || str === '-' || str === '+') { + // Check for invalid combinations of signs and decimal separators + if (/^[+-]?[.٫,]?$/.test(str)) { return false; } const value = parseFloat(str.replace(',', '.')); diff --git a/test/validators.test.js b/test/validators.test.js index 12c5fc2..a72dcb4 100644 --- a/test/validators.test.js +++ b/test/validators.test.js @@ -4896,6 +4896,188 @@ describe('Validators', () => { }); }); + it('should validate CSS color strings', () => { + test({ + validator: 'isColor', + valid: [ + // Hex colors + '#ff0000', + '#FF0000', + '#f00', + '#F00', + '#ff0000ff', + '#FF0000FF', + '#f00f', + '#F00F', + 'ff0000', + 'FF0000', + 'f00', + 'F00', + 'ff0000ff', + 'FF0000FF', + 'f00f', + 'F00F', + // RGB colors + 'rgb(255,0,0)', + 'rgb(255, 0, 0)', + 'rgb(100%, 0%, 0%)', + 'rgb(100%, 0%, 0%)', + 'rgba(255,0,0,1)', + 'rgba(255, 0, 0, 1)', + 'rgba(255,0,0,0.5)', + 'rgba(100%, 0%, 0%, 0.5)', + // HSL colors + 'hsl(0,100%,50%)', + 'hsl(0, 100%, 50%)', + 'hsl(360, 100%, 50%)', + 'hsla(0,100%,50%,1)', + 'hsla(0, 100%, 50%, 1)', + 'hsla(0,100%,50%,0.5)', + 'hsl(270 60% 70%)', + 'hsla(270, 60%, 50%, .15)', + ], + invalid: [ + 'invalid', + '#gg0000', + '#ff00', + 'rgb(256,0,0)', + 'rgb(255,0)', + 'hsl(361,100%,50%)', + 'hsl(0,101%,50%)', + 'rgba(255,0,0,2)', + 'hsla(0,100%,50%,2)', + '', + 'red', + 'blue', + '#', + 'rgb()', + 'hsl()', + ], + }); + }); + + it('should validate CSS color strings with specific format', () => { + test({ + validator: 'isColor', + args: [{ format: 'hex' }], + valid: [ + '#ff0000', + '#FF0000', + '#f00', + '#F00', + '#ff0000ff', + 'ff0000', + 'f00', + ], + invalid: [ + 'rgb(255,0,0)', + 'hsl(0,100%,50%)', + '#gg0000', + 'invalid', + ], + }); + + test({ + validator: 'isColor', + args: [{ format: 'rgb' }], + valid: [ + 'rgb(255,0,0)', + 'rgb(255, 0, 0)', + 'rgba(255,0,0,1)', + 'rgba(255, 0, 0, 0.5)', + ], + invalid: [ + '#ff0000', + 'hsl(0,100%,50%)', + 'rgb(256,0,0)', + 'invalid', + ], + }); + + test({ + validator: 'isColor', + args: [{ format: 'hsl' }], + valid: [ + 'hsl(0,100%,50%)', + 'hsl(0, 100%, 50%)', + 'hsla(0,100%,50%,1)', + 'hsla(0, 100%, 50%, 0.5)', + ], + invalid: [ + '#ff0000', + 'rgb(255,0,0)', + 'hsl(361,100%,50%)', + 'invalid', + ], + }); + + test({ + validator: 'isColor', + args: [{ format: 'hexa' }], + valid: [ + '#ff0000ff', + '#FF0000FF', + '#f00f', + '#F00F', + 'ff0000ff', + 'f00f', + ], + invalid: [ + '#ff0000', + '#f00', + 'rgb(255,0,0)', + 'hsl(0,100%,50%)', + 'invalid', + ], + }); + + test({ + validator: 'isColor', + args: [{ format: 'rgba' }], + valid: [ + 'rgba(255,0,0,1)', + 'rgba(255, 0, 0, 0.5)', + 'rgba(100%, 0%, 0%, 0.5)', + ], + invalid: [ + 'rgb(255,0,0)', + '#ff0000', + 'hsl(0,100%,50%)', + 'rgba(255,0,0,2)', + 'invalid', + ], + }); + + test({ + validator: 'isColor', + args: [{ format: 'hsla' }], + valid: [ + 'hsla(0,100%,50%,1)', + 'hsla(0, 100%, 50%, 0.5)', + 'hsla(270, 60%, 50%, .15)', + ], + invalid: [ + 'hsl(0,100%,50%)', + '#ff0000', + 'rgb(255,0,0)', + 'hsla(0,100%,50%,2)', + 'invalid', + ], + }); + + test({ + validator: 'isColor', + args: [{ format: 'invalid' }], + valid: [], + invalid: [ + '#ff0000', + 'rgb(255,0,0)', + 'hsl(0,100%,50%)', + 'invalid', + ], + }); + }); + it('should validate rgb color strings', () => { test({ validator: 'isRgbColor',