123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537 |
- /**
- * These are commonly used parsers for CSS Values they take a string to parse
- * and return a string after it's been converted, if needed
- */
- "use strict";
- const { resolve: resolveColor, utils } = require("@asamuzakjp/css-color");
- const { asciiLowercase } = require("./utils/strings");
- const { cssCalc, isColor, isGradient, splitValue } = utils;
- // CSS global values
- // @see https://drafts.csswg.org/css-cascade-5/#defaulting-keywords
- const GLOBAL_VALUE = Object.freeze(["initial", "inherit", "unset", "revert", "revert-layer"]);
- // Numeric data types
- const NUM_TYPE = Object.freeze({
- UNDEFINED: 0,
- VAR: 1,
- NUMBER: 2,
- PERCENT: 4,
- LENGTH: 8,
- ANGLE: 0x10,
- CALC: 0x20
- });
- // System colors
- // @see https://drafts.csswg.org/css-color/#css-system-colors
- // @see https://drafts.csswg.org/css-color/#deprecated-system-colors
- const SYS_COLOR = Object.freeze([
- "accentcolor",
- "accentcolortext",
- "activeborder",
- "activecaption",
- "activetext",
- "appworkspace",
- "background",
- "buttonborder",
- "buttonface",
- "buttonhighlight",
- "buttonshadow",
- "buttontext",
- "canvas",
- "canvastext",
- "captiontext",
- "field",
- "fieldtext",
- "graytext",
- "highlight",
- "highlighttext",
- "inactiveborder",
- "inactivecaption",
- "inactivecaptiontext",
- "infobackground",
- "infotext",
- "linktext",
- "mark",
- "marktext",
- "menu",
- "menutext",
- "scrollbar",
- "selecteditem",
- "selecteditemtext",
- "threeddarkshadow",
- "threedface",
- "threedhighlight",
- "threedlightshadow",
- "threedshadow",
- "visitedtext",
- "window",
- "windowframe",
- "windowtext"
- ]);
- // Regular expressions
- const DIGIT = "(?:0|[1-9]\\d*)";
- const NUMBER = `[+-]?(?:${DIGIT}(?:\\.\\d*)?|\\.\\d+)(?:e-?${DIGIT})?`;
- const unitRegEx = new RegExp(`^(${NUMBER})([a-z]+|%)?$`, "i");
- const urlRegEx = /^url\(\s*((?:[^)]|\\\))*)\s*\)$/;
- const keywordRegEx = /^[a-z]+(?:-[a-z]+)*$/i;
- const stringRegEx = /^("[^"]*"|'[^']*')$/;
- const varRegEx = /^var\(/;
- const varContainedRegEx = /(?<=[*/\s(])var\(/;
- const calcRegEx =
- /^(?:a?(?:cos|sin|tan)|abs|atan2|calc|clamp|exp|hypot|log|max|min|mod|pow|rem|round|sign|sqrt)\(/;
- const functionRegEx = /^([a-z][a-z\d]*(?:-[a-z\d]+)*)\(/i;
- const getNumericType = function getNumericType(val) {
- if (varRegEx.test(val)) {
- return NUM_TYPE.VAR;
- }
- if (calcRegEx.test(val)) {
- return NUM_TYPE.CALC;
- }
- if (unitRegEx.test(val)) {
- const [, , unit] = unitRegEx.exec(val);
- if (!unit) {
- return NUM_TYPE.NUMBER;
- }
- if (unit === "%") {
- return NUM_TYPE.PERCENT;
- }
- if (/^(?:[cm]m|[dls]?v(?:[bhiw]|max|min)|in|p[ctx]|q|r?(?:[cl]h|cap|e[mx]|ic))$/i.test(unit)) {
- return NUM_TYPE.LENGTH;
- }
- if (/^(?:deg|g?rad|turn)$/i.test(unit)) {
- return NUM_TYPE.ANGLE;
- }
- }
- return NUM_TYPE.UNDEFINED;
- };
- // Prepare stringified value.
- exports.prepareValue = function prepareValue(value, globalObject = globalThis) {
- // `null` is converted to an empty string.
- // @see https://webidl.spec.whatwg.org/#LegacyNullToEmptyString
- if (value === null) {
- return "";
- }
- const type = typeof value;
- switch (type) {
- case "string":
- return value.trim();
- case "number":
- return value.toString();
- case "undefined":
- return "undefined";
- case "symbol":
- throw new globalObject.TypeError("Can not convert symbol to string.");
- default: {
- const str = value.toString();
- if (typeof str === "string") {
- return str;
- }
- throw new globalObject.TypeError(`Can not convert ${type} to string.`);
- }
- }
- };
- exports.hasVarFunc = function hasVarFunc(val) {
- return varRegEx.test(val) || varContainedRegEx.test(val);
- };
- exports.parseNumber = function parseNumber(val, restrictToPositive = false) {
- if (val === "") {
- return "";
- }
- const type = getNumericType(val);
- switch (type) {
- case NUM_TYPE.VAR:
- return val;
- case NUM_TYPE.CALC:
- return cssCalc(val, {
- format: "specifiedValue"
- });
- case NUM_TYPE.NUMBER: {
- const num = parseFloat(val);
- if (restrictToPositive && num < 0) {
- return;
- }
- return `${num}`;
- }
- default:
- if (varContainedRegEx.test(val)) {
- return val;
- }
- }
- };
- exports.parseLength = function parseLength(val, restrictToPositive = false) {
- if (val === "") {
- return "";
- }
- const type = getNumericType(val);
- switch (type) {
- case NUM_TYPE.VAR:
- return val;
- case NUM_TYPE.CALC:
- return cssCalc(val, {
- format: "specifiedValue"
- });
- case NUM_TYPE.NUMBER:
- if (parseFloat(val) === 0) {
- return "0px";
- }
- return;
- case NUM_TYPE.LENGTH: {
- const [, numVal, unit] = unitRegEx.exec(val);
- const num = parseFloat(numVal);
- if (restrictToPositive && num < 0) {
- return;
- }
- return `${num}${asciiLowercase(unit)}`;
- }
- default:
- if (varContainedRegEx.test(val)) {
- return val;
- }
- }
- };
- exports.parsePercent = function parsePercent(val, restrictToPositive = false) {
- if (val === "") {
- return "";
- }
- const type = getNumericType(val);
- switch (type) {
- case NUM_TYPE.VAR:
- return val;
- case NUM_TYPE.CALC:
- return cssCalc(val, {
- format: "specifiedValue"
- });
- case NUM_TYPE.NUMBER:
- if (parseFloat(val) === 0) {
- return "0%";
- }
- return;
- case NUM_TYPE.PERCENT: {
- const [, numVal, unit] = unitRegEx.exec(val);
- const num = parseFloat(numVal);
- if (restrictToPositive && num < 0) {
- return;
- }
- return `${num}${asciiLowercase(unit)}`;
- }
- default:
- if (varContainedRegEx.test(val)) {
- return val;
- }
- }
- };
- // Either a length or a percent.
- exports.parseMeasurement = function parseMeasurement(val, restrictToPositive = false) {
- if (val === "") {
- return "";
- }
- const type = getNumericType(val);
- switch (type) {
- case NUM_TYPE.VAR:
- return val;
- case NUM_TYPE.CALC:
- return cssCalc(val, {
- format: "specifiedValue"
- });
- case NUM_TYPE.NUMBER:
- if (parseFloat(val) === 0) {
- return "0px";
- }
- return;
- case NUM_TYPE.LENGTH:
- case NUM_TYPE.PERCENT: {
- const [, numVal, unit] = unitRegEx.exec(val);
- const num = parseFloat(numVal);
- if (restrictToPositive && num < 0) {
- return;
- }
- return `${num}${asciiLowercase(unit)}`;
- }
- default:
- if (varContainedRegEx.test(val)) {
- return val;
- }
- }
- };
- exports.parseAngle = function parseAngle(val, normalizeDeg = false) {
- if (val === "") {
- return "";
- }
- const type = getNumericType(val);
- switch (type) {
- case NUM_TYPE.VAR:
- return val;
- case NUM_TYPE.CALC:
- return cssCalc(val, {
- format: "specifiedValue"
- });
- case NUM_TYPE.NUMBER:
- if (parseFloat(val) === 0) {
- return "0deg";
- }
- return;
- case NUM_TYPE.ANGLE: {
- let [, numVal, unit] = unitRegEx.exec(val);
- numVal = parseFloat(numVal);
- unit = asciiLowercase(unit);
- if (unit === "deg") {
- if (normalizeDeg && numVal < 0) {
- while (numVal < 0) {
- numVal += 360;
- }
- }
- numVal %= 360;
- }
- return `${numVal}${unit}`;
- }
- default:
- if (varContainedRegEx.test(val)) {
- return val;
- }
- }
- };
- exports.parseUrl = function parseUrl(val) {
- if (val === "") {
- return val;
- }
- const res = urlRegEx.exec(val);
- if (!res) {
- return;
- }
- let str = res[1];
- // If it starts with single or double quotes, does it end with the same?
- if ((str[0] === '"' || str[0] === "'") && str[0] !== str[str.length - 1]) {
- return;
- }
- if (str[0] === '"' || str[0] === "'") {
- str = str.substr(1, str.length - 2);
- }
- let urlstr = "";
- let escaped = false;
- for (let i = 0; i < str.length; i++) {
- switch (str[i]) {
- case "\\":
- if (escaped) {
- urlstr += "\\\\";
- escaped = false;
- } else {
- escaped = true;
- }
- break;
- case "(":
- case ")":
- case " ":
- case "\t":
- case "\n":
- case "'":
- if (!escaped) {
- return;
- }
- urlstr += str[i];
- escaped = false;
- break;
- case '"':
- if (!escaped) {
- return;
- }
- urlstr += '\\"';
- escaped = false;
- break;
- default:
- urlstr += str[i];
- escaped = false;
- }
- }
- return `url("${urlstr}")`;
- };
- exports.parseString = function parseString(val) {
- if (val === "") {
- return "";
- }
- if (!stringRegEx.test(val)) {
- return;
- }
- val = val.substr(1, val.length - 2);
- let str = "";
- let escaped = false;
- for (let i = 0; i < val.length; i++) {
- switch (val[i]) {
- case "\\":
- if (escaped) {
- str += "\\\\";
- escaped = false;
- } else {
- escaped = true;
- }
- break;
- case '"':
- str += '\\"';
- escaped = false;
- break;
- default:
- str += val[i];
- escaped = false;
- }
- }
- return `"${str}"`;
- };
- exports.parseKeyword = function parseKeyword(val, validKeywords = []) {
- if (val === "") {
- return "";
- }
- if (varRegEx.test(val)) {
- return val;
- }
- val = asciiLowercase(val.toString());
- if (validKeywords.includes(val) || GLOBAL_VALUE.includes(val)) {
- return val;
- }
- };
- exports.parseColor = function parseColor(val) {
- if (val === "") {
- return "";
- }
- if (varRegEx.test(val)) {
- return val;
- }
- if (/^[a-z]+$/i.test(val)) {
- const v = asciiLowercase(val);
- if (SYS_COLOR.includes(v)) {
- return v;
- }
- }
- const res = resolveColor(val, {
- format: "specifiedValue"
- });
- if (res) {
- return res;
- }
- return exports.parseKeyword(val);
- };
- exports.parseImage = function parseImage(val) {
- if (val === "") {
- return "";
- }
- if (varRegEx.test(val)) {
- return val;
- }
- if (keywordRegEx.test(val)) {
- return exports.parseKeyword(val, ["none"]);
- }
- const values = splitValue(val, {
- delimiter: ",",
- preserveComment: varContainedRegEx.test(val)
- });
- let isImage = Boolean(values.length);
- for (let i = 0; i < values.length; i++) {
- const image = values[i];
- if (image === "") {
- return "";
- }
- if (isGradient(image) || /^(?:none|inherit)$/i.test(image)) {
- continue;
- }
- const imageUrl = exports.parseUrl(image);
- if (imageUrl) {
- values[i] = imageUrl;
- } else {
- isImage = false;
- break;
- }
- }
- if (isImage) {
- return values.join(", ");
- }
- };
- exports.parseFunction = function parseFunction(val) {
- if (val === "") {
- return {
- name: null,
- value: ""
- };
- }
- if (functionRegEx.test(val) && val.endsWith(")")) {
- if (varRegEx.test(val) || varContainedRegEx.test(val)) {
- return {
- name: "var",
- value: val
- };
- }
- const [, name] = functionRegEx.exec(val);
- const value = val
- .replace(new RegExp(`^${name}\\(`), "")
- .replace(/\)$/, "")
- .trim();
- return {
- name,
- value
- };
- }
- };
- exports.parseShorthand = function parseShorthand(val, shorthandFor, preserve = false) {
- const obj = {};
- if (val === "" || exports.hasVarFunc(val)) {
- for (const [property] of shorthandFor) {
- obj[property] = "";
- }
- return obj;
- }
- const key = exports.parseKeyword(val);
- if (key) {
- if (key === "inherit") {
- return obj;
- }
- return;
- }
- const parts = splitValue(val);
- const shorthandArr = [...shorthandFor];
- for (const part of parts) {
- let partValid = false;
- for (let i = 0; i < shorthandArr.length; i++) {
- const [property, value] = shorthandArr[i];
- if (value.isValid(part)) {
- partValid = true;
- obj[property] = value.parse(part);
- if (!preserve) {
- shorthandArr.splice(i, 1);
- break;
- }
- }
- }
- if (!partValid) {
- return;
- }
- }
- return obj;
- };
- // Returns `false` for global values, e.g. "inherit".
- exports.isValidColor = function isValidColor(val) {
- if (SYS_COLOR.includes(asciiLowercase(val))) {
- return true;
- }
- return isColor(val);
- };
- // Splits value into an array.
- // @see https://github.com/asamuzaK/cssColor/blob/main/src/js/util.ts
- exports.splitValue = splitValue;
|