Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
61 changes: 44 additions & 17 deletions src/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -84,10 +84,30 @@ export default function parse (str, options) {
}
}
else {
format = ColorSpace.findFormat({ name, type: "function" });
// If there are commas, try to find a legacy format first
if (env.parsed.commas) {
format = ColorSpace.findFormat({ name: `${name}_legacy`, type: "function" });
}
if (!format) {
format = ColorSpace.findFormat({ name, type: "function" });
}
space = format.space;
}

// Validate the parsed type per coord against the allowed types per coord in the format.
// Need to cut off the fourth element from `types` (i.e the alpha channel) as `types` only has entries for color coordinate.
for (const [index, parsedType] of types.slice(0, 3).entries()) {
const formatTypes = format.coords[index];

// If the format doesn't have the parsed type, it's invalid syntax (e.g. HSL's legacy syntax doesn't support <number> for saturation or lightness).
if (parsedType && !formatTypes.some(({ type }) => type === parsedType)) {
const allowedTypes = formatTypes.map(({ type }) => type);
throw new TypeError(
`Cannot parse ${env.str}. Coordinate ${index} uses type ${parsedType}, but expects ${allowedTypes.join(" | ")}`,
);
}
}

if (meta) {
Object.assign(meta, {
format,
Expand Down Expand Up @@ -189,41 +209,44 @@ export const regex = {
/**
* Parse a single function argument
* @param {string} rawArg
* @returns {{value: number, meta: ArgumentMeta}}
* @returns {{value: string | number | null, meta: ArgumentMeta}}
*/
export function parseArgument (rawArg) {
/** @type {Partial<ArgumentMeta>} */
let meta = {};
meta.none = false;
let unit = rawArg.match(regex.unitValue)?.[0];
/** @type {string | number} */
let value = (meta.raw = rawArg);
meta.raw = rawArg;
/** @type {string | number | null} */
let value;

if (unit) {
// It’s a dimension token
meta.type = unit === "%" ? "<percentage>" : "<angle>";
meta.unit = unit;
meta.unitless = Number(value.slice(0, -unit.length)); // unitless number
meta.unitless = Number(rawArg.slice(0, -unit.length)); // unitless number

value = meta.unitless * units[unit];
}
else if (regex.number.test(value)) {
else if (regex.number.test(rawArg)) {
// It's a number
// Convert numerical args to numbers
value = Number(value);
value = Number(rawArg);
meta.type = "<number>";
}
else if (value === "none") {
else if (rawArg === "none") {
value = null;
meta.none = true;
}
else if (value === "NaN" || value === "calc(NaN)") {
else if (rawArg === "NaN" || rawArg === "calc(NaN)") {
value = NaN;
meta.type = "<number>";
}
else {
value = rawArg;
meta.type = "<ident>";
}

return { value: /** @type {number} */ (value), meta: /** @type {ArgumentMeta} */ (meta) };
return { value, meta: /** @satisfies {ArgumentMeta} */ (meta) };
}

/**
Expand All @@ -242,19 +265,23 @@ export function parseFunction (str) {

if (parts) {
// It is a function, parse args
/** @type {Array<string | number>} */
let args = [];
/** @type {ArgumentMeta[]} */
let argMeta = [];
let lastAlpha = false;
let name = parts[1].toLowerCase();
let rawName = parts[1];
let rawArgs = parts[2];
let name = rawName.toLowerCase();

let separators = parts[2].replace(regex.singleArgument, ($0, rawArg) => {
let separators = rawArgs.replace(regex.singleArgument, ($0, rawArg) => {
let { value, meta } = parseArgument(rawArg);

if (
// If there's a slash here, it's modern syntax
$0.startsWith("/")
// If there's still elements to process after there's already 3 in `args` (and the we're not dealing with "color()"), it's likely to be a legacy color like "hsl(0, 0%, 0%, 0.5)"
|| (name !== "color" && args.length === 3)
// If there's still elements to process after there's already 3 in `args` (and there are commas), it's a legacy syntax like "hsl(0, 0%, 0%, 0.5)"
|| (args.length === 3 && rawArgs.includes(","))
) {
// It's alpha
lastAlpha = true;
Expand All @@ -271,8 +298,8 @@ export function parseFunction (str) {
argMeta,
lastAlpha,
commas: separators.includes(","),
rawName: parts[1],
rawArgs: parts[2],
rawName,
rawArgs,
};
}
}
9 changes: 9 additions & 0 deletions src/spaces/hsl.js
Original file line number Diff line number Diff line change
Expand Up @@ -92,5 +92,14 @@ export default new ColorSpace({
commas: true,
alpha: true,
},
hsl_legacy: {
coords: ["<number> | <angle>", "<percentage>", "<percentage>"],
commas: true,
},
hsla_legacy: {
coords: ["<number> | <angle>", "<percentage>", "<percentage>"],
commas: true,
alpha: true,
},
},
});
2 changes: 1 addition & 1 deletion src/types.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,7 @@ export interface ParseOptions {

export interface ParseFunctionReturn {
name: string;
args: string[];
args: (string | number)[];
argMeta: ArgumentMeta[];
lastAlpha: boolean;
rawName: string;
Expand Down
8 changes: 2 additions & 6 deletions test/parse.js
Original file line number Diff line number Diff line change
Expand Up @@ -578,16 +578,12 @@ const tests = {
{
name: "legacy syntax, <number> saturation/lightness, no alpha (#428, #648)",
args: ["hsl(0, 0, 0)"],
throws: true,
// TODO: #428. This currently parses successfully but shouldn't because the legacy syntax doesn't allow `<number>` for saturation or lightness.
skip: true,
throws: TypeError,
},
{
name: "legacy syntax, <number> saturation/lightness, alpha (#428, #648)",
args: ["hsl(0, 0, 0, 0.5)"],
throws: true,
// TODO: #428. This currently parses successfully but shouldn't because the legacy syntax doesn't allow `<number>` for saturation or lightness.
skip: true,
throws: TypeError,
},
{
name: "modern syntax, <percentage> saturation/lightness, no alpha (#428, #648)",
Expand Down
Loading