Skip to content

Instantly share code, notes, and snippets.

@u1f992
Created December 3, 2024 14:17
Show Gist options
  • Save u1f992/136c413a14c5775ce28e5f64b73d59f9 to your computer and use it in GitHub Desktop.
Save u1f992/136c413a14c5775ce28e5f64b73d59f9 to your computer and use it in GitHub Desktop.
/**
* https://drafts.csswg.org/css-color-5/#device-cmyk
*
* @param {string} input
* @returns {{c:number,m:number,y:number,k:number,a:number}|null}
*/
function parseDeviceCmyk(input) {
/**
* https://www.w3.org/TR/css-syntax/#newline
*/
const newline = /\x0a/;
/**
* https://www.w3.org/TR/css-syntax/#whitespace
*/
const whitespace = new RegExp(`(?:${newline.source})|\x09|\x20`);
const comment = /\/\*[\s\S]*?\*\//;
/**
* FIXME: どこかで定義されていないか確認する
*/
const tokenSeparator = new RegExp(
`(?:${whitespace.source})|(?:${comment.source})`
);
/**
* A hash mark (#) indicates that the preceding type, word, or group occurs one or more times, separated by comma tokens (which may optionally be surrounded by [white space](https://www.w3.org/TR/css-syntax/#whitespace) and/or comments). It may optionally be followed by the curly brace forms, above, to indicate precisely how many times the repetition occurs, like `<length>#{1,4}`. - https://drafts.csswg.org/css-values-4/#mult-comma
*/
const multComma = (/** @type {RegExp} */ re, /** @type {number} */ a) => {
return new RegExp(
Array(a)
.fill(`(${re.source})`)
.join(`(?:${tokenSeparator.source})*?,(?:${tokenSeparator.source})*?`)
);
};
/**
* A single number in curly braces ({A}) indicates that the preceding type, word, or group occurs A times. - https://drafts.csswg.org/css-values-4/#mult-num
*
* FIXME: "occurs A times"の意味がよくわからない。空白あるいはコメントを1つ以上挟む?
*/
const multNum = (/** @type {RegExp} */ re, /** @type {number} */ a) => {
return new RegExp(
Array(a).fill(`(${re.source})`).join(`(?:${tokenSeparator.source})+`)
);
};
/**
* When written literally, a number is either an [integer](https://drafts.csswg.org/css-values-4/#integer), or zero or more decimal digits followed by a dot (.) followed by one or more decimal digits; optionally, it can be concluded by the letter "e" or "E" followed by an integer indicating the base-ten exponent in [scientific notation](https://en.wikipedia.org/wiki/Scientific_notation). - https://drafts.csswg.org/css-values-4/#number-value
*/
const number = /(?:(?:[\+-]?\d+?)|(?:[\+-]?\d*?\.\d+?))(?:[eE][\+-]?\d+?)?/;
/**
* When written literally, a percentage consists of a [number](https://drafts.csswg.org/css-values-4/#number) immediately followed by a percent sign %. - https://drafts.csswg.org/css-values-4/#percentage-value
*/
const percentage = new RegExp(`(?:${number.source})%`);
/**
* https://drafts.csswg.org/css-color-4/#typedef-color-alpha-value
*/
const alphaValue = new RegExp(
`(?:${number.source})|(?:${percentage.source})`
);
/**
* `<cmyk-component> = <number> | <percentage> | none`
* https://drafts.csswg.org/css-color-5/#device-cmyk
*/
const cmykComponent = new RegExp(
`(?:${number.source})|(?:${percentage.source})|(?:none)`
);
/**
* `<legacy-device-cmyk-syntax> = device-cmyk( <number>#{4} )`
* https://drafts.csswg.org/css-color-5/#device-cmyk
*/
const legacyDeviceCmykSyntax = new RegExp(
`device-cmyk\\((?:${tokenSeparator.source})*(?:${
multComma(number, 4).source
})(?:${tokenSeparator.source})*\\)`
);
/**
* `<modern-device-cmyk-syntax> = device-cmyk( <cmyk-component>{4} [ / [ <alpha-value> | none ] ]? )`
* https://drafts.csswg.org/css-color-5/#device-cmyk
*/
const modernDeviceCmykSyntax = new RegExp(
`device-cmyk\\((?:${tokenSeparator.source})*(?:${
multNum(cmykComponent, 4).source
})(?:(?:${tokenSeparator.source})*/(?:${tokenSeparator.source})*((?:${
alphaValue.source
})|none))?(?:${tokenSeparator.source})*\\)`
);
/**
* `device-cmyk() = <legacy-device-cmyk-syntax> | <modern-device-cmyk-syntax>`
* https://drafts.csswg.org/css-color-5/#device-cmyk
*
* Legacy
* - c: ret[1]
* - m: ret[2]
* - y: ret[3]
* - k: ret[4]
*
* Modern
* - c: ret[5]
* - m: ret[6]
* - y: ret[7]
* - k: ret[8]
* - a?: ret[9]
*/
const deviceCmyk = new RegExp(
`^(?:${legacyDeviceCmykSyntax.source})|(?:${modernDeviceCmykSyntax.source})$`
);
const parseSaturate = (/** @type {string} */ val) =>
Math.max(
0,
Math.min(
val.endsWith("%")
? parseFloat(val.slice(0, -1)) / 100
: parseFloat(val),
1
)
);
const match = deviceCmyk.exec(input);
return !match
? null
: match[1]
? {
c: parseSaturate(match[1]),
m: parseSaturate(match[2]),
y: parseSaturate(match[3]),
k: parseSaturate(match[4]),
a: 1,
}
: {
// FIXME: `none`の場合の挙動が不明
c: match[5] === "none" ? 0 : parseSaturate(match[5]),
m: match[6] === "none" ? 0 : parseSaturate(match[6]),
y: match[7] === "none" ? 0 : parseSaturate(match[7]),
k: match[8] === "none" ? 0 : parseSaturate(match[8]),
a:
typeof match[9] === "undefined" || match[9] === "none"
? 1
: parseSaturate(match[9]),
};
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment