Merge pull request #284423 from microsoft/dev/dmitriv/hsla-colors-regex

Add tests and CSS 4 syntax support
This commit is contained in:
Dmitriy Vasyura 2025-12-24 22:37:52 -08:00 committed by GitHub
commit 83f180cca3
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 76 additions and 5 deletions

View File

@ -100,8 +100,8 @@ function _findMatches(model: IDocumentColorComputerTarget | string, regex: RegEx
function computeColors(model: IDocumentColorComputerTarget): IColorInformation[] { function computeColors(model: IDocumentColorComputerTarget): IColorInformation[] {
const result: IColorInformation[] = []; const result: IColorInformation[] = [];
// Early validation for RGB and HSL // Early validation for RGB and HSL (including CSS Level 4 syntax with / separator)
const initialValidationRegex = /\b(rgb|rgba|hsl|hsla)(\([0-9\s,.\%]*\))|^(#)([A-Fa-f0-9]{3})\b|^(#)([A-Fa-f0-9]{4})\b|^(#)([A-Fa-f0-9]{6})\b|^(#)([A-Fa-f0-9]{8})\b|(?<=['"\s])(#)([A-Fa-f0-9]{3})\b|(?<=['"\s])(#)([A-Fa-f0-9]{4})\b|(?<=['"\s])(#)([A-Fa-f0-9]{6})\b|(?<=['"\s])(#)([A-Fa-f0-9]{8})\b/gm; const initialValidationRegex = /\b(rgb|rgba|hsl|hsla)(\([0-9\s,.\%\/]*\))|^(#)([A-Fa-f0-9]{3})\b|^(#)([A-Fa-f0-9]{4})\b|^(#)([A-Fa-f0-9]{6})\b|^(#)([A-Fa-f0-9]{8})\b|(?<=['"\s])(#)([A-Fa-f0-9]{3})\b|(?<=['"\s])(#)([A-Fa-f0-9]{4})\b|(?<=['"\s])(#)([A-Fa-f0-9]{6})\b|(?<=['"\s])(#)([A-Fa-f0-9]{8})\b/gm;
const initialValidationMatches = _findMatches(model, initialValidationRegex); const initialValidationMatches = _findMatches(model, initialValidationRegex);
// Potential colors have been found, validate the parameters // Potential colors have been found, validate the parameters
@ -115,16 +115,19 @@ function computeColors(model: IDocumentColorComputerTarget): IColorInformation[]
} }
let colorInformation; let colorInformation;
if (colorScheme === 'rgb') { if (colorScheme === 'rgb') {
const regexParameters = /^\(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*\)$/gm; // Supports both comma-separated (rgb(255, 0, 0)) and CSS Level 4 space-separated syntax (rgb(255 0 0))
const regexParameters = /^\(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*[\s,]\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*[\s,]\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*\)$/gm;
colorInformation = _findRGBColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), false); colorInformation = _findRGBColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), false);
} else if (colorScheme === 'rgba') { } else if (colorScheme === 'rgba') {
const regexParameters = /^\(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*,\s*(0[.][0-9]+|[.][0-9]+|[01][.]|[01])\s*\)$/gm; // Supports both comma-separated (rgba(255, 0, 0, 0.5)) and CSS Level 4 syntax (rgba(255 0 0 / 0.5))
const regexParameters = /^\(\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*[\s,]\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*[\s,]\s*(25[0-5]|2[0-4][0-9]|1[0-9]{2}|[1-9][0-9]|[0-9])\s*(?:[\s,]|[\s]*\/)\s*(0[.][0-9]+|[.][0-9]+|[01][.]|[01])\s*\)$/gm;
colorInformation = _findRGBColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), true); colorInformation = _findRGBColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), true);
} else if (colorScheme === 'hsl') { } else if (colorScheme === 'hsl') {
const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*\)$/gm; const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*\)$/gm;
colorInformation = _findHSLColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), false); colorInformation = _findHSLColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), false);
} else if (colorScheme === 'hsla') { } else if (colorScheme === 'hsla') {
const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(0[.][0-9]+|[.][0-9]+|[01][.]0*|[01])\s*\)$/gm; // Supports both comma-separated (hsla(253, 100%, 50%, 0.5)) and CSS Level 4 syntax (hsla(253 100% 50% / 0.5))
const regexParameters = /^\(\s*((?:360(?:\.0+)?|(?:36[0]|3[0-5][0-9]|[12][0-9][0-9]|[1-9]?[0-9])(?:\.\d+)?))\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*[\s,]\s*(100(?:\.0+)?|\d{1,2}[.]\d*|\d{1,2})%\s*(?:[\s,]|[\s]*\/)\s*(0[.][0-9]+|[.][0-9]+|[01][.]0*|[01])\s*\)$/gm;
colorInformation = _findHSLColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), true); colorInformation = _findHSLColorInformation(_findRange(model, initialMatch), _findMatches(colorParameters, regexParameters), true);
} else if (colorScheme === '#') { } else if (colorScheme === '#') {
colorInformation = _findHexColorInformation(_findRange(model, initialMatch), colorScheme + colorParameters); colorInformation = _findHexColorInformation(_findRange(model, initialMatch), colorScheme + colorParameters);

View File

@ -120,4 +120,72 @@ suite('Default Document Colors Computer', () => {
assert.strictEqual(colors.length, 1, 'Should detect one hsl color'); assert.strictEqual(colors.length, 1, 'Should detect one hsl color');
}); });
test('hsl with decimal hue values should work', () => {
// Test case from issue #180436 comment
const testCases = [
{ content: 'hsl(253.5, 100%, 50%)', name: 'decimal hue' },
{ content: 'hsl(360.0, 50%, 50%)', name: '360.0 hue' },
{ content: 'hsl(100.5, 50.5%, 50.5%)', name: 'all decimals' },
{ content: 'hsl(0.5, 50%, 50%)', name: 'small decimal hue' },
{ content: 'hsl(359.9, 100%, 50%)', name: 'near-max decimal hue' }
];
testCases.forEach(testCase => {
const model = new TestDocumentModel(`const color = ${testCase.content};`);
const colors = computeDefaultDocumentColors(model);
assert.strictEqual(colors.length, 1, `Should detect hsl color with ${testCase.name}: ${testCase.content}`);
});
});
test('hsla with decimal values should work', () => {
const testCases = [
{ content: 'hsla(253.5, 100%, 50%, 0.5)', name: 'decimal hue with alpha' },
{ content: 'hsla(360.0, 50.5%, 50.5%, 1)', name: 'all decimals with alpha 1' },
{ content: 'hsla(0.5, 50%, 50%, 0.25)', name: 'small decimal hue with alpha' }
];
testCases.forEach(testCase => {
const model = new TestDocumentModel(`const color = ${testCase.content};`);
const colors = computeDefaultDocumentColors(model);
assert.strictEqual(colors.length, 1, `Should detect hsla color with ${testCase.name}: ${testCase.content}`);
});
});
test('hsl with space separator (CSS Level 4 syntax) should work', () => {
// CSS Level 4 allows space-separated values instead of comma-separated
const testCases = [
{ content: 'hsl(253 100% 50%)', name: 'space-separated' },
{ content: 'hsl(253.5 100% 50%)', name: 'space-separated with decimal hue' },
{ content: 'hsla(253 100% 50% / 0.5)', name: 'hsla with slash separator for alpha' },
{ content: 'hsla(253.5 100% 50% / 0.5)', name: 'hsla with decimal hue and slash separator' },
{ content: 'hsla(253 100% 50% / 1)', name: 'hsla with slash and alpha 1' }
];
testCases.forEach(testCase => {
const model = new TestDocumentModel(`const color = ${testCase.content};`);
const colors = computeDefaultDocumentColors(model);
assert.strictEqual(colors.length, 1, `Should detect hsl color with ${testCase.name}: ${testCase.content}`);
});
});
test('rgb and rgba with CSS Level 4 space-separated syntax should work', () => {
// CSS Level 4 allows space-separated values for RGB/RGBA
const testCases = [
{ content: 'rgb(255 0 0)', name: 'rgb space-separated' },
{ content: 'rgb(128 128 128)', name: 'rgb space-separated gray' },
{ content: 'rgba(255 0 0 / 0.5)', name: 'rgba with slash separator for alpha' },
{ content: 'rgba(128 128 128 / 0.8)', name: 'rgba gray with slash separator' },
{ content: 'rgba(255 0 0 / 1)', name: 'rgba with slash and alpha 1' },
// Traditional comma syntax should still work
{ content: 'rgb(255, 0, 0)', name: 'rgb comma-separated (traditional)' },
{ content: 'rgba(255, 0, 0, 0.5)', name: 'rgba comma-separated (traditional)' }
];
testCases.forEach(testCase => {
const model = new TestDocumentModel(`const color = ${testCase.content};`);
const colors = computeDefaultDocumentColors(model);
assert.strictEqual(colors.length, 1, `Should detect rgb/rgba color with ${testCase.name}: ${testCase.content}`);
});
});
}); });