UNPKG

cssstyle

Version:

CSSStyleDeclaration Object Model implementation

503 lines (480 loc) 11.5 kB
/** * 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 { 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 const SYS_COLOR = Object.freeze([ "accentcolor", "accentcolortext", "activetext", "buttonborder", "buttonface", "buttontext", "canvas", "canvastext", "field", "fieldtext", "graytext", "highlight", "highlighttext", "linktext", "mark", "marktext", "visitedtext", "activeborder", "activecaption", "appworkspace", "background", "buttonhighlight", "buttonshadow", "captiontext", "inactiveborder", "inactivecaption", "inactivecaptiontext", "infobackground", "infotext", "menu", "menutext", "scrollbar", "threeddarkshadow", "threedface", "threedhighlight", "threedlightshadow", "threedshadow", "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 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}${unit.toLowerCase()}`; } 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}${unit.toLowerCase()}`; } 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}${unit.toLowerCase()}`; } 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 = unit.toLowerCase(); 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 = val.toString().toLowerCase(); 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) && SYS_COLOR.includes(val.toLowerCase())) { return val; } 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.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(val.toLowerCase())) { 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;