diff options
| author | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
|---|---|---|
| committer | Adam Mathes <adam@adammathes.com> | 2026-02-13 21:34:48 -0800 |
| commit | 76cb9c2a39d477a64824a985ade40507e3bbade1 (patch) | |
| tree | 41e997aa9c6f538d3a136af61dae9424db2005a9 /vanilla/node_modules/cssstyle/lib/normalize.js | |
| parent | 819a39a21ac992b1393244a4c283bbb125208c69 (diff) | |
| download | neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.gz neko-76cb9c2a39d477a64824a985ade40507e3bbade1.tar.bz2 neko-76cb9c2a39d477a64824a985ade40507e3bbade1.zip | |
feat(vanilla): add testing infrastructure and tests (NK-wjnczv)
Diffstat (limited to 'vanilla/node_modules/cssstyle/lib/normalize.js')
| -rw-r--r-- | vanilla/node_modules/cssstyle/lib/normalize.js | 1574 |
1 files changed, 1574 insertions, 0 deletions
diff --git a/vanilla/node_modules/cssstyle/lib/normalize.js b/vanilla/node_modules/cssstyle/lib/normalize.js new file mode 100644 index 0000000..bc39b70 --- /dev/null +++ b/vanilla/node_modules/cssstyle/lib/normalize.js @@ -0,0 +1,1574 @@ +"use strict"; + +const propertyDefinitions = require("./generated/propertyDefinitions"); +const { hasVarFunc, isGlobalKeyword, isValidPropertyValue, splitValue } = require("./parsers"); +const background = require("./properties/background"); +const backgroundColor = require("./properties/backgroundColor"); +const backgroundSize = require("./properties/backgroundSize"); +const border = require("./properties/border"); +const borderWidth = require("./properties/borderWidth"); +const borderStyle = require("./properties/borderStyle"); +const borderColor = require("./properties/borderColor"); +const borderTop = require("./properties/borderTop"); +const borderRight = require("./properties/borderRight"); +const borderBottom = require("./properties/borderBottom"); +const borderLeft = require("./properties/borderLeft"); +const flex = require("./properties/flex"); +const font = require("./properties/font"); +const margin = require("./properties/margin"); +const padding = require("./properties/padding"); + +/* constants */ +const BORDER_IMAGE = "border-image"; +const TOP = "top"; +const RIGHT = "right"; +const BOTTOM = "bottom"; +const LEFT = "left"; +const WIDTH = "width"; +const STYLE = "style"; +const COLOR = "color"; +const NONE = "none"; +const TRBL_INDICES = { + [TOP]: 0, + [RIGHT]: 1, + [BOTTOM]: 2, + [LEFT]: 3 +}; + +/* shorthands */ +const shorthandProperties = new Map([ + [background.property, background], + [ + border.property, + { + definition: border.definition, + parse: border.parse, + shorthandFor: new Map([ + ...border.shorthandFor, + ...border.positionShorthandFor, + [BORDER_IMAGE, null] + ]) + } + ], + [borderWidth.property, borderWidth], + [borderStyle.property, borderStyle], + [borderColor.property, borderColor], + [borderTop.property, borderTop], + [borderRight.property, borderRight], + [borderBottom.property, borderBottom], + [borderLeft.property, borderLeft], + ["flex", flex], + ["font", font], + ["margin", margin], + ["padding", padding] +]); + +/* borders */ +const borderProperties = new Set([ + border.property, + BORDER_IMAGE, + ...border.shorthandFor.keys(), + ...border.positionShorthandFor.keys(), + ...borderTop.shorthandFor.keys(), + ...borderRight.shorthandFor.keys(), + ...borderBottom.shorthandFor.keys(), + ...borderLeft.shorthandFor.keys() +]); +const borderPositions = new Set([TOP, RIGHT, BOTTOM, LEFT]); +const borderLines = new Set([WIDTH, STYLE, COLOR]); + +/** + * Ensures consistent object shape. + * + * @param {string} property - The property name. + * @param {string} [value=""] - The property value. + * @param {string} [priority=""] - The priority. + * @returns {Object} The property item object. + */ +const createPropertyItem = (property, value = "", priority = "") => ({ + property, + value, + priority +}); + +/** + * Retrieves a property item from the map or creates a default one if it doesn't exist. + * + * @param {string} property - The name of the property. + * @param {Map} properties - The map containing all properties. + * @returns {Object} The property item containing name, value, and priority. + */ +const getPropertyItem = (property, properties) => { + const propertyItem = properties.get(property) ?? createPropertyItem(property); + return propertyItem; +}; + +/** + * Calculates the value for a specific position (top, right, bottom, left) + * based on the array of values provided for a shorthand property. + * + * @param {string[]} positionValues - The values extracted from the shorthand property. + * @param {string} position - The specific position (top, right, bottom, left) to retrieve. + * @returns {string} The calculated value for the position. + */ +const getPositionValue = (positionValues, position) => { + const [val1, val2, val3, val4] = positionValues; + const index = TRBL_INDICES[position] ?? -1; + // If a specific position (top, right, bottom, left) is requested. + if (index !== -1) { + switch (positionValues.length) { + case 2: { + // Index 0 (Top) & 2 (Bottom) -> val1 + // Index 1 (Right) & 3 (Left) -> val2 + return index % 2 === 0 ? val1 : val2; + } + case 3: { + // Index 0 (Top) -> val1 + // Index 1 (Right) & 3 (Left) -> val2 + // Index 2 (Bottom) -> val3 + if (index === 2) { + return val3; + } + return index % 2 === 0 ? val1 : val2; + } + case 4: { + return positionValues[index]; + } + case 1: + default: { + return val1; + } + } + } + // Fallback logic for when no specific position is requested. + switch (positionValues.length) { + case 2: { + if (val1 === val2) { + return val1; + } + return `${val1} ${val2}`; + } + case 3: { + if (val1 === val3) { + if (val1 === val2) { + return val1; + } + return `${val1} ${val2}`; + } + return `${val1} ${val2} ${val3}`; + } + case 4: { + if (val2 === val4) { + if (val1 === val3) { + if (val1 === val2) { + return val1; + } + return `${val1} ${val2}`; + } + return `${val1} ${val2} ${val3}`; + } + return `${val1} ${val2} ${val3} ${val4}`; + } + case 1: + default: { + return val1; + } + } +}; + +/** + * Replaces the background shorthand property based on individual longhand values. + * + * @param {string} property - The specific background longhand property being updated. + * @param {Map} properties - The map of all properties. + * @param {Object} opt - Parsing options including global object and configurations. + * @returns {string} The constructed background shorthand string. + */ +const replaceBackgroundShorthand = (property, properties, opt) => { + const { value: propertyValue } = properties.get(property); + const parsedValue = background.shorthandFor.get(property).parse(propertyValue, opt); + const values = splitValue(parsedValue, { + delimiter: "," + }); + const { value: shorthandValue } = properties.get(background.property); + const bgValues = background.parse(shorthandValue, opt); + const bgLength = bgValues.length; + if (property === backgroundColor.property) { + bgValues[bgLength - 1][property] = parsedValue[0]; + } else { + for (let i = 0; i < bgLength; i++) { + bgValues[i][property] = values[i]; + } + } + const backgrounds = []; + for (const bgValue of bgValues) { + const bg = []; + for (const [longhand, value] of Object.entries(bgValue)) { + if (!value || value === background.initialValues.get(longhand)) { + continue; + } + if (longhand === backgroundSize.property) { + bg.push(`/ ${value}`); + } else { + bg.push(value); + } + } + backgrounds.push(bg.join(" ")); + } + return backgrounds.join(", "); +}; + +/** + * Checks if a property value matches the value within a border shorthand. + * + * @param {string} property - The property to check. + * @param {string} value - The value to compare. + * @param {string} shorthandValue - The shorthand string to parse and compare against. + * @param {Object} [opt={}] - Parsing options. + * @returns {boolean} True if the value matches the shorthand's value. + */ +const matchesBorderShorthandValue = (property, value, shorthandValue, opt = {}) => { + const { globalObject, options } = opt; + const obj = border.parse(shorthandValue, { + globalObject, + options + }); + if (Object.hasOwn(obj, property)) { + return value === obj[property]; + } + return value === border.initialValues.get(property); +}; + +/** + * Replaces or updates a value within a border shorthand string. + * + * @param {string} value - The new value to insert. + * @param {string} shorthandValue - The existing shorthand string. + * @param {Object} [opt={}] - Parsing options. + * @returns {string} The updated border shorthand string. + */ +const replaceBorderShorthandValue = (value, shorthandValue, opt = {}) => { + const { globalObject, options } = opt; + const borderFirstInitialKey = border.initialValues.keys().next().value; + const borderFirstInitialValue = border.initialValues.get(borderFirstInitialKey); + const parseOpt = { + globalObject, + options + }; + const valueObj = border.parse(value, parseOpt); + const shorthandObj = shorthandValue + ? border.parse(shorthandValue, parseOpt) + : { + [borderFirstInitialKey]: borderFirstInitialValue + }; + const keys = border.shorthandFor.keys(); + for (const key of keys) { + const initialValue = border.initialValues.get(key); + let parsedValue = initialValue; + if (Object.hasOwn(valueObj, key)) { + parsedValue = valueObj[key]; + } + if (parsedValue === initialValue) { + if (key === borderFirstInitialKey) { + if (!Object.hasOwn(shorthandObj, key)) { + shorthandObj[key] = parsedValue; + } + } else { + delete shorthandObj[key]; + } + } else { + shorthandObj[key] = parsedValue; + if ( + shorthandObj[borderFirstInitialKey] && + shorthandObj[borderFirstInitialKey] === borderFirstInitialValue + ) { + delete shorthandObj[borderFirstInitialKey]; + } + } + } + return Object.values(shorthandObj).join(" "); +}; + +/** + * Replaces a value at a specific position (top, right, bottom, left) within a position shorthand. + * + * @param {string} value - The new value to set. + * @param {string[]} positionValues - The array of existing position values. + * @param {string} position - The position to update. + * @returns {string} The updated shorthand string. + */ +const replacePositionValue = (value, positionValues, position) => { + const index = TRBL_INDICES[position] ?? -1; + let currentValues = positionValues; + if (index !== -1) { + // Loop for reducing array length (instead of recursion) + while (true) { + const [val1, val2, val3, val4] = currentValues; + switch (currentValues.length) { + case 2: { + if (val1 === val2) { + currentValues = [val1]; + continue; + } + switch (index) { + // Top + case 0: { + if (val1 === value) { + return currentValues.join(" "); + } + return `${value} ${val2} ${val1}`; + } + // Right + case 1: { + if (val2 === value) { + return currentValues.join(" "); + } + return `${val1} ${value} ${val1} ${val2}`; + } + // Bottom + case 2: { + if (val1 === value) { + return currentValues.join(" "); + } + return `${val1} ${val2} ${value}`; + } + // Left + case 3: + default: { + if (val2 === value) { + return currentValues.join(" "); + } + return `${val1} ${val2} ${val1} ${value}`; + } + } + } + case 3: { + if (val1 === val3) { + currentValues = [val1, val2]; + continue; + } + switch (index) { + // Top + case 0: { + if (val1 === value) { + return currentValues.join(" "); + } else if (val3 === value) { + return `${value} ${val2}`; + } + return `${value} ${val2} ${val3}`; + } + // Right + case 1: { + if (val2 === value) { + return currentValues.join(" "); + } + return `${val1} ${value} ${val3} ${val2}`; + } + // Bottom + case 2: { + if (val3 === value) { + return currentValues.join(" "); + } else if (val1 === value) { + return `${val1} ${val2}`; + } + return `${val1} ${val2} ${value}`; + } + // Left + case 3: + default: { + if (val2 === value) { + return currentValues.join(" "); + } + return `${val1} ${val2} ${val3} ${value}`; + } + } + } + case 4: { + if (val2 === val4) { + currentValues = [val1, val2, val3]; + continue; + } + switch (index) { + // Top + case 0: { + if (val1 === value) { + return currentValues.join(" "); + } + return `${value} ${val2} ${val3} ${val4}`; + } + // Right + case 1: { + if (val2 === value) { + return currentValues.join(" "); + } else if (val4 === value) { + return `${val1} ${value} ${val3}`; + } + return `${val1} ${value} ${val3} ${val4}`; + } + // Bottom + case 2: { + if (val3 === value) { + return currentValues.join(" "); + } + return `${val1} ${val2} ${value} ${val4}`; + } + // Left + case 3: + default: { + if (val4 === value) { + return currentValues.join(" "); + } else if (val2 === value) { + return `${val1} ${val2} ${val3}`; + } + return `${val1} ${val2} ${val3} ${value}`; + } + } + } + case 1: + default: { + const [val] = currentValues; + if (val === value) { + return currentValues.join(" "); + } + switch (index) { + // Top + case 0: { + return `${value} ${val} ${val}`; + } + // Right + case 1: { + return `${val} ${value} ${val} ${val}`; + } + // Bottom + case 2: { + return `${val} ${val} ${value}`; + } + // Left + case 3: + default: { + return `${val} ${val} ${val} ${value}`; + } + } + } + } + } + } + // Fallback logic for when no specific position is requested. + const [val1, val2, val3, val4] = currentValues; + switch (currentValues.length) { + case 2: { + if (val1 === val2) { + return val1; + } + return `${val1} ${val2}`; + } + case 3: { + if (val1 === val3) { + if (val1 === val2) { + return val1; + } + return `${val1} ${val2}`; + } + return `${val1} ${val2} ${val3}`; + } + case 4: { + if (val2 === val4) { + if (val1 === val3) { + if (val1 === val2) { + return val1; + } + return `${val1} ${val2}`; + } + return `${val1} ${val2} ${val3}`; + } + return `${val1} ${val2} ${val3} ${val4}`; + } + case 1: + default: { + return val1; + } + } +}; + +/** + * Handles border property preparation when the value is a string. + * + * @param {Object} params - The parameters object. + * @param {string} params.property - The property name. + * @param {string} params.value - The property value. + * @param {string} params.priority - The property priority. + * @param {Map} params.properties - The map of properties. + * @param {Object} params.parts - The split property name parts. + * @param {Object} params.opt - Parsing options. + * @param {Map} params.borderItems - The map to store processed border items. + */ +const prepareBorderStringValue = ({ + property, + value, + priority, + properties, + parts, + opt, + borderItems +}) => { + const { prop1, prop2, prop3 } = parts; + const { globalObject, options } = opt; + const parseOpt = { + globalObject, + options + }; + const shorthandItem = getPropertyItem(border.property, properties); + const imageItem = getPropertyItem(BORDER_IMAGE, properties); + // Handle longhand properties. + if (prop3) { + const lineProperty = `${prop1}-${prop3}`; + const lineItem = getPropertyItem(lineProperty, properties); + const positionProperty = `${prop1}-${prop2}`; + const positionItem = getPropertyItem(positionProperty, properties); + const longhandProperty = `${prop1}-${prop2}-${prop3}`; + const longhandItem = getPropertyItem(longhandProperty, properties); + longhandItem.value = value; + longhandItem.priority = priority; + const propertyValue = hasVarFunc(value) ? "" : value; + if (propertyValue === "") { + shorthandItem.value = ""; + lineItem.value = ""; + positionItem.value = ""; + } else if (isGlobalKeyword(propertyValue)) { + if (shorthandItem.value !== propertyValue) { + shorthandItem.value = ""; + } + if (lineItem.value !== propertyValue) { + lineItem.value = ""; + } + if (positionItem.value !== propertyValue) { + positionItem.value = ""; + } + } else { + if ( + shorthandItem.value && + !matchesBorderShorthandValue(lineProperty, propertyValue, shorthandItem.value, parseOpt) + ) { + shorthandItem.value = ""; + } + if (lineItem.value) { + lineItem.value = replacePositionValue(propertyValue, splitValue(lineItem.value), prop2); + } + if ( + positionItem.value && + !matchesBorderShorthandValue(lineProperty, propertyValue, positionItem.value, parseOpt) + ) { + positionItem.value = ""; + } + } + borderItems.set(border.property, shorthandItem); + borderItems.set(BORDER_IMAGE, imageItem); + borderItems.set(lineProperty, lineItem); + borderItems.set(positionProperty, positionItem); + borderItems.set(longhandProperty, longhandItem); + // Handle side-specific border shorthands (border-top, border-right, border-bottom, border-left). + } else if (prop2 && borderPositions.has(prop2)) { + const lineWidthProperty = `${prop1}-width`; + const lineWidthItem = getPropertyItem(lineWidthProperty, properties); + const lineStyleProperty = `${prop1}-style`; + const lineStyleItem = getPropertyItem(lineStyleProperty, properties); + const lineColorProperty = `${prop1}-color`; + const lineColorItem = getPropertyItem(lineColorProperty, properties); + const positionProperty = `${prop1}-${prop2}`; + const positionItem = getPropertyItem(positionProperty, properties); + positionItem.value = value; + positionItem.priority = priority; + const propertyValue = hasVarFunc(value) ? "" : value; + if (propertyValue === "") { + shorthandItem.value = ""; + lineWidthItem.value = ""; + lineStyleItem.value = ""; + lineColorItem.value = ""; + } else if (isGlobalKeyword(propertyValue)) { + if (shorthandItem.value !== propertyValue) { + shorthandItem.value = ""; + } + if (lineWidthItem.value !== propertyValue) { + lineWidthItem.value = ""; + } + if (lineStyleItem.value !== propertyValue) { + lineStyleItem.value = ""; + } + if (lineColorItem.value !== propertyValue) { + lineColorItem.value = ""; + } + } else { + if ( + shorthandItem.value && + !matchesBorderShorthandValue(property, propertyValue, shorthandItem.value, parseOpt) + ) { + shorthandItem.value = ""; + } + if ( + lineWidthItem.value && + isValidPropertyValue(lineWidthProperty, propertyValue, globalObject) + ) { + lineWidthItem.value = propertyValue; + } + if ( + lineStyleItem.value && + isValidPropertyValue(lineStyleProperty, propertyValue, globalObject) + ) { + lineStyleItem.value = propertyValue; + } + if ( + lineColorItem.value && + isValidPropertyValue(lineColorProperty, propertyValue, globalObject) + ) { + lineColorItem.value = propertyValue; + } + } + for (const line of borderLines) { + const longhandProperty = `${prop1}-${prop2}-${line}`; + const longhandItem = getPropertyItem(longhandProperty, properties); + longhandItem.value = propertyValue; + longhandItem.priority = priority; + borderItems.set(longhandProperty, longhandItem); + } + borderItems.set(border.property, shorthandItem); + borderItems.set(BORDER_IMAGE, imageItem); + borderItems.set(lineWidthProperty, lineWidthItem); + borderItems.set(lineStyleProperty, lineStyleItem); + borderItems.set(lineColorProperty, lineColorItem); + borderItems.set(positionProperty, positionItem); + // Handle property-specific border shorthands (border-width, border-style, border-color). + } else if (prop2 && borderLines.has(prop2)) { + const lineProperty = `${prop1}-${prop2}`; + const lineItem = getPropertyItem(lineProperty, properties); + lineItem.value = value; + lineItem.priority = priority; + const propertyValue = hasVarFunc(value) ? "" : value; + if (propertyValue === "") { + shorthandItem.value = ""; + } else if (isGlobalKeyword(propertyValue)) { + if (shorthandItem.value !== propertyValue) { + shorthandItem.value = ""; + } + } + for (const position of borderPositions) { + const positionProperty = `${prop1}-${position}`; + const positionItem = getPropertyItem(positionProperty, properties); + const longhandProperty = `${prop1}-${position}-${prop2}`; + const longhandItem = getPropertyItem(longhandProperty, properties); + if (propertyValue) { + positionItem.value = replaceBorderShorthandValue( + propertyValue, + positionItem.value, + parseOpt + ); + } else { + positionItem.value = ""; + } + longhandItem.value = propertyValue; + longhandItem.priority = priority; + borderItems.set(positionProperty, positionItem); + borderItems.set(longhandProperty, longhandItem); + } + borderItems.set(border.property, shorthandItem); + borderItems.set(BORDER_IMAGE, imageItem); + borderItems.set(lineProperty, lineItem); + // Handle border shorthand. + } else { + const propertyValue = hasVarFunc(value) ? "" : value; + imageItem.value = propertyValue ? NONE : ""; + for (const line of borderLines) { + const lineProperty = `${prop1}-${line}`; + const lineItem = getPropertyItem(lineProperty, properties); + lineItem.value = propertyValue; + lineItem.priority = priority; + borderItems.set(lineProperty, lineItem); + } + for (const position of borderPositions) { + const positionProperty = `${prop1}-${position}`; + const positionItem = getPropertyItem(positionProperty, properties); + positionItem.value = propertyValue; + positionItem.priority = priority; + borderItems.set(positionProperty, positionItem); + for (const line of borderLines) { + const longhandProperty = `${positionProperty}-${line}`; + const longhandItem = getPropertyItem(longhandProperty, properties); + longhandItem.value = propertyValue; + longhandItem.priority = priority; + borderItems.set(longhandProperty, longhandItem); + } + } + borderItems.set(property, shorthandItem); + borderItems.set(BORDER_IMAGE, imageItem); + } +}; + +/** + * Handles border property preparation when the value is an array. + * + * @param {Object} params - The parameters object. + * @param {Array} params.value - The property value. + * @param {string} params.priority - The property priority. + * @param {Map} params.properties - The map of properties. + * @param {Object} params.parts - The split property name parts. + * @param {Object} params.opt - Parsing options. + * @param {Map} params.borderItems - The map to store processed border items. + */ +const prepareBorderArrayValue = ({ value, priority, properties, parts, opt, borderItems }) => { + const { prop1, prop2 } = parts; + const { globalObject, options } = opt; + const parseOpt = { + globalObject, + options + }; + if (!value.length || !borderLines.has(prop2)) { + return; + } + const shorthandItem = getPropertyItem(border.property, properties); + const imageItem = getPropertyItem(BORDER_IMAGE, properties); + const lineProperty = `${prop1}-${prop2}`; + const lineItem = getPropertyItem(lineProperty, properties); + if (value.length === 1) { + const [propertyValue] = value; + if (shorthandItem.value) { + if (hasVarFunc(shorthandItem.value)) { + shorthandItem.value = ""; + } else if (propertyValue) { + shorthandItem.value = replaceBorderShorthandValue( + propertyValue, + shorthandItem.value, + parseOpt + ); + } + } + } else { + shorthandItem.value = ""; + } + lineItem.value = value.join(" "); + lineItem.priority = priority; + const positionValues = {}; + const [val1, val2, val3, val4] = value; + switch (value.length) { + case 2: { + positionValues.top = val1; + positionValues.right = val2; + positionValues.bottom = val1; + positionValues.left = val2; + break; + } + case 3: { + positionValues.top = val1; + positionValues.right = val2; + positionValues.bottom = val3; + positionValues.left = val2; + break; + } + case 4: { + positionValues.top = val1; + positionValues.right = val2; + positionValues.bottom = val3; + positionValues.left = val4; + break; + } + case 1: + default: { + positionValues.top = val1; + positionValues.right = val1; + positionValues.bottom = val1; + positionValues.left = val1; + } + } + for (const position of borderPositions) { + const positionProperty = `${prop1}-${position}`; + const positionItem = getPropertyItem(positionProperty, properties); + if (positionItem.value && positionValues[position]) { + positionItem.value = replaceBorderShorthandValue( + positionValues[position], + positionItem.value, + parseOpt + ); + } + const longhandProperty = `${positionProperty}-${prop2}`; + const longhandItem = getPropertyItem(longhandProperty, properties); + longhandItem.value = positionValues[position]; + longhandItem.priority = priority; + borderItems.set(positionProperty, positionItem); + borderItems.set(longhandProperty, longhandItem); + } + borderItems.set(border.property, shorthandItem); + borderItems.set(BORDER_IMAGE, imageItem); + borderItems.set(lineProperty, lineItem); +}; + +/** + * Handles border property preparation when the value is an object. + * + * @param {Object} params - The parameters object. + * @param {string} params.property - The property name. + * @param {Object} params.value - The property value. + * @param {string} params.priority - The property priority. + * @param {Map} params.properties - The map of properties. + * @param {Object} params.parts - The split property name parts. + * @param {Object} params.opt - Parsing options. + * @param {Map} params.borderItems - The map to store processed border items. + */ +const prepareBorderObjectValue = ({ + property, + value, + priority, + properties, + parts, + opt, + borderItems +}) => { + const { prop1, prop2 } = parts; + const { globalObject, options } = opt; + const parseOpt = { + globalObject, + options + }; + const imageItem = getPropertyItem(BORDER_IMAGE, properties); + // Handle position shorthands. + if (prop2) { + if (!borderPositions.has(prop2)) { + return; + } + const shorthandItem = getPropertyItem(border.property, properties); + const lineWidthProperty = `${prop1}-width`; + const lineWidthItem = getPropertyItem(lineWidthProperty, properties); + const lineStyleProperty = `${prop1}-style`; + const lineStyleItem = getPropertyItem(lineStyleProperty, properties); + const lineColorProperty = `${prop1}-color`; + const lineColorItem = getPropertyItem(lineColorProperty, properties); + const positionProperty = `${prop1}-${prop2}`; + const positionItem = getPropertyItem(positionProperty, properties); + if (shorthandItem.value) { + for (const positionValue of Object.values(value)) { + if (!matchesBorderShorthandValue(property, positionValue, shorthandItem.value, parseOpt)) { + shorthandItem.value = ""; + break; + } + } + } + positionItem.value = Object.values(value).join(" "); + positionItem.priority = priority; + for (const line of borderLines) { + const longhandProperty = `${prop1}-${prop2}-${line}`; + const longhandItem = getPropertyItem(longhandProperty, properties); + const itemValue = Object.hasOwn(value, longhandProperty) + ? value[longhandProperty] + : border.initialValues.get(`${prop1}-${line}`); + if (line === WIDTH && lineWidthItem.value) { + lineWidthItem.value = replacePositionValue( + itemValue, + splitValue(lineWidthItem.value), + prop2 + ); + } else if (line === STYLE && lineStyleItem.value) { + lineStyleItem.value = replacePositionValue( + itemValue, + splitValue(lineStyleItem.value), + prop2 + ); + } else if (line === COLOR && lineColorItem.value) { + lineColorItem.value = replacePositionValue( + itemValue, + splitValue(lineColorItem.value), + prop2 + ); + } + longhandItem.value = itemValue; + longhandItem.priority = priority; + borderItems.set(longhandProperty, longhandItem); + } + borderItems.set(border.property, shorthandItem); + borderItems.set(BORDER_IMAGE, imageItem); + borderItems.set(lineWidthProperty, lineWidthItem); + borderItems.set(lineStyleProperty, lineStyleItem); + borderItems.set(lineColorProperty, lineColorItem); + borderItems.set(positionProperty, positionItem); + // Handle border shorthand. + } else { + const shorthandItem = getPropertyItem(prop1, properties); + const lineWidthProperty = `${prop1}-width`; + const lineWidthItem = getPropertyItem(lineWidthProperty, properties); + const lineStyleProperty = `${prop1}-style`; + const lineStyleItem = getPropertyItem(lineStyleProperty, properties); + const lineColorProperty = `${prop1}-color`; + const lineColorItem = getPropertyItem(lineColorProperty, properties); + const propertyValue = Object.values(value).join(" "); + shorthandItem.value = propertyValue; + shorthandItem.priority = priority; + imageItem.value = propertyValue ? NONE : ""; + if (Object.hasOwn(value, lineWidthProperty)) { + lineWidthItem.value = value[lineWidthProperty]; + } else { + lineWidthItem.value = border.initialValues.get(lineWidthProperty); + } + lineWidthItem.priority = priority; + if (Object.hasOwn(value, lineStyleProperty)) { + lineStyleItem.value = value[lineStyleProperty]; + } else { + lineStyleItem.value = border.initialValues.get(lineStyleProperty); + } + lineStyleItem.priority = priority; + if (Object.hasOwn(value, lineColorProperty)) { + lineColorItem.value = value[lineColorProperty]; + } else { + lineColorItem.value = border.initialValues.get(lineColorProperty); + } + lineColorItem.priority = priority; + for (const position of borderPositions) { + const positionProperty = `${prop1}-${position}`; + const positionItem = getPropertyItem(positionProperty, properties); + positionItem.value = propertyValue; + positionItem.priority = priority; + for (const line of borderLines) { + const longhandProperty = `${positionProperty}-${line}`; + const longhandItem = getPropertyItem(longhandProperty, properties); + const lineProperty = `${prop1}-${line}`; + if (Object.hasOwn(value, lineProperty)) { + longhandItem.value = value[lineProperty]; + } else { + longhandItem.value = border.initialValues.get(lineProperty); + } + longhandItem.priority = priority; + borderItems.set(longhandProperty, longhandItem); + } + borderItems.set(positionProperty, positionItem); + } + borderItems.set(property, shorthandItem); + borderItems.set(BORDER_IMAGE, imageItem); + borderItems.set(lineWidthProperty, lineWidthItem); + borderItems.set(lineStyleProperty, lineStyleItem); + borderItems.set(lineColorProperty, lineColorItem); + } +}; + +/** + * Prepares border properties by splitting shorthands and handling updates. + * + * @param {string} property - The border property name. + * @param {string|Array|Object} value - The value of the property. + * @param {string} priority - The priority of the property (e.g., "important"). + * @param {Map} properties - The map of all properties. + * @param {Object} [opt={}] - Parsing options. + * @returns {Map|undefined} A map of expanded or updated border properties. + */ +const prepareBorderProperties = (property, value, priority, properties, opt = {}) => { + if (typeof property !== "string" || value === null) { + return; + } + if (!property.startsWith(border.property)) { + return; + } + let prop2, prop3; + if (property.length > border.property.length) { + // Check if next char is '-' + if (property.charCodeAt(border.property.length) !== 45) { + return; + } + // property is like "border-..." + const remainder = property.substring(border.property.length + 1); + const hyphenIndex = remainder.indexOf("-"); + if (hyphenIndex !== -1) { + prop2 = remainder.substring(0, hyphenIndex); + prop3 = remainder.substring(hyphenIndex + 1); + } else { + prop2 = remainder; + } + } + if ( + (borderPositions.has(prop2) && prop3 && !borderLines.has(prop3)) || + (borderLines.has(prop2) && prop3) + ) { + return; + } + const parts = { + prop1: border.property, + prop2, + prop3 + }; + const borderItems = new Map(); + if (typeof value === "string") { + prepareBorderStringValue({ + property, + value, + priority, + properties, + parts, + opt, + borderItems + }); + } else if (Array.isArray(value)) { + prepareBorderArrayValue({ + value, + priority, + properties, + parts, + opt, + borderItems + }); + } else if (value && typeof value === "object") { + prepareBorderObjectValue({ + property, + value, + priority, + properties, + parts, + opt, + borderItems + }); + } + if (!borderItems.has(border.property)) { + return; + } + const borderProps = new Map([[border.property, borderItems.get(border.property)]]); + for (const line of borderLines) { + const lineProperty = `${border.property}-${line}`; + const lineItem = borderItems.get(lineProperty) ?? getPropertyItem(lineProperty, properties); + borderProps.set(lineProperty, lineItem); + } + for (const position of borderPositions) { + const positionProperty = `${border.property}-${position}`; + const positionItem = + borderItems.get(positionProperty) ?? getPropertyItem(positionProperty, properties); + borderProps.set(positionProperty, positionItem); + for (const line of borderLines) { + const longhandProperty = `${border.property}-${position}-${line}`; + const longhandItem = + borderItems.get(longhandProperty) ?? getPropertyItem(longhandProperty, properties); + borderProps.set(longhandProperty, longhandItem); + } + } + const borderImageItem = borderItems.get(BORDER_IMAGE) ?? createPropertyItem(BORDER_IMAGE); + borderProps.set(BORDER_IMAGE, borderImageItem); + return borderProps; +}; + +/** + * Generates a border line shorthand property if all line components are present. + * + * @param {Map} items - The map of collected property items. + * @param {string} property - The shorthand property name to generate. + * @param {string} [priority=""] - The priority of the property. + * @returns {Array} A key-value pair for the generated property. + */ +const generateBorderLineShorthand = (items, property, priority = "") => { + const values = []; + for (const [, item] of items) { + const { value: itemValue } = item; + values.push(itemValue); + } + const value = getPositionValue(values); + return [property, createPropertyItem(property, value, priority)]; +}; + +/** + * Generates a border position shorthand property if all position components are present. + * + * @param {Map} items - The map of collected property items. + * @param {string} property - The shorthand property name to generate. + * @param {string} [priority=""] - The priority of the property. + * @returns {Array} A key-value pair for the generated property. + */ +const generateBorderPositionShorthand = (items, property, priority = "") => { + const values = []; + for (const [, item] of items) { + const { value: itemValue } = item; + values.push(itemValue); + } + const value = values.join(" "); + return [property, createPropertyItem(property, value, priority)]; +}; + +/** + * Generates a border shorthand property if all components match. + * + * @param {Array} items - The collection of property values. + * @param {string} property - The shorthand property name to generate. + * @param {string} [priority=""] - The priority of the property. + * @returns {Array|undefined} A key-value pair for the generated property or undefined. + */ +const generateBorderShorthand = (items, property, priority = "") => { + const values = new Set(items); + if (values.size === 1) { + const value = values.keys().next().value; + return [property, createPropertyItem(property, value, priority)]; + } +}; + +const borderCollectionConfig = { + [WIDTH]: { + shorthand: borderWidth.property, + generator: generateBorderLineShorthand + }, + [STYLE]: { + shorthand: borderStyle.property, + generator: generateBorderLineShorthand + }, + [COLOR]: { + shorthand: borderColor.property, + generator: generateBorderLineShorthand + }, + [TOP]: { + shorthand: borderTop.property, + generator: generateBorderPositionShorthand + }, + [RIGHT]: { + shorthand: borderRight.property, + generator: generateBorderPositionShorthand + }, + [BOTTOM]: { + shorthand: borderBottom.property, + generator: generateBorderPositionShorthand + }, + [LEFT]: { + shorthand: borderLeft.property, + generator: generateBorderPositionShorthand + } +}; + +/** + * Processes and consolidates border-related longhands into shorthands where possible. + * + * @param {Map} properties - The map of current properties. + * @returns {Map} The updated map with consolidated border properties. + */ +const prepareBorderShorthands = (properties) => { + const borderCollections = {}; + for (const key of Object.keys(borderCollectionConfig)) { + borderCollections[key] = { + ...borderCollectionConfig[key], + items: new Map(), + priorityItems: new Map() + }; + } + for (const [property, item] of properties) { + const { priority, value } = item; + let positionPart, linePart; + // We can assume property starts with "border-" + const firstHyphen = property.indexOf("-"); + if (firstHyphen !== -1) { + const remainder = property.substring(firstHyphen + 1); + const secondHyphen = remainder.indexOf("-"); + if (secondHyphen !== -1) { + positionPart = remainder.substring(0, secondHyphen); + linePart = remainder.substring(secondHyphen + 1); + } else { + positionPart = remainder; + linePart = undefined; + } + } + if (linePart && borderCollections[linePart]) { + const collection = borderCollections[linePart]; + if (priority) { + collection.priorityItems.set(property, { property, value, priority }); + } else { + collection.items.set(property, { property, value, priority }); + } + } + if (positionPart && borderCollections[positionPart]) { + const collection = borderCollections[positionPart]; + if (priority) { + collection.priorityItems.set(property, { property, value, priority }); + } else { + collection.items.set(property, { property, value, priority }); + } + } + } + const shorthandItems = []; + const shorthandPriorityItems = []; + for (const [key, collection] of Object.entries(borderCollections)) { + const { shorthand, generator, items, priorityItems } = collection; + const requiredSize = borderLines.has(key) ? 4 : 3; + if (items.size === requiredSize) { + const [property, item] = generator(items, shorthand) ?? []; + if (property && item) { + properties.set(property, item); + if (borderPositions.has(key) && properties.has(BORDER_IMAGE)) { + const { value: imageValue } = properties.get(BORDER_IMAGE); + if (imageValue === NONE) { + shorthandItems.push(item.value); + } + } + } + } else if (priorityItems.size === requiredSize) { + const [property, item] = generator(priorityItems, shorthand, "important") ?? []; + if (property && item) { + properties.set(property, item); + if (borderPositions.has(key) && properties.has(BORDER_IMAGE)) { + const { value: imageValue } = properties.get(BORDER_IMAGE); + if (imageValue === NONE) { + shorthandPriorityItems.push(item.value); + } + } + } + } + } + const mixedPriorities = shorthandItems.length && shorthandPriorityItems.length; + const imageItem = createPropertyItem(BORDER_IMAGE, NONE); + if (shorthandItems.length === 4) { + const [property, item] = generateBorderShorthand(shorthandItems, border.property) ?? []; + if (property && item) { + properties.set(property, item); + properties.delete(BORDER_IMAGE); + properties.set(BORDER_IMAGE, imageItem); + } + } else if (shorthandPriorityItems.length === 4) { + const [property, item] = + generateBorderShorthand(shorthandPriorityItems, border.property, "important") ?? []; + if (property && item) { + properties.set(property, item); + properties.delete(BORDER_IMAGE); + properties.set(BORDER_IMAGE, imageItem); + } + } else if (properties.has(BORDER_IMAGE)) { + const { value: imageValue } = properties.get(BORDER_IMAGE); + if (imageValue === NONE) { + if (mixedPriorities) { + properties.delete(BORDER_IMAGE); + properties.set(BORDER_IMAGE, imageItem); + } else { + properties.delete(BORDER_IMAGE); + } + } + } + if (mixedPriorities) { + const items = []; + const priorityItems = []; + for (const item of properties) { + const [, { priority }] = item; + if (priority) { + priorityItems.push(item); + } else { + items.push(item); + } + } + const firstPropertyKey = properties.keys().next().value; + const { priority: firstPropertyPriority } = properties.get(firstPropertyKey); + if (firstPropertyPriority) { + return new Map([...priorityItems, ...items]); + } + return new Map([...items, ...priorityItems]); + } + if (properties.has(BORDER_IMAGE)) { + properties.delete(BORDER_IMAGE); + properties.set(BORDER_IMAGE, imageItem); + } + return properties; +}; + +/** + * Processes shorthand properties from the shorthands map. + * + * @param {Map} shorthands - The map containing shorthand property groups. + * @returns {Map} A map of processed shorthand properties. + */ +const processShorthandProperties = (shorthands) => { + const shorthandItems = new Map(); + for (const [property, item] of shorthands) { + const shorthandItem = shorthandProperties.get(property); + if (item.size === shorthandItem.shorthandFor.size && shorthandItem.position) { + const positionValues = []; + let priority = ""; + for (const { value: longhandValue, priority: longhandPriority } of item.values()) { + positionValues.push(longhandValue); + if (longhandPriority) { + priority = longhandPriority; + } + } + const value = getPositionValue(positionValues, shorthandItem.position); + shorthandItems.set(property, createPropertyItem(property, value, priority)); + } + } + return shorthandItems; +}; + +/** + * Updates the longhand properties map with a new property item. + * If a property with normal priority already exists, it will be overwritten by the new item. + * If the existing property has "important" priority, it will not be overwritten. + * + * @param {string} property - The CSS property name. + * @param {Object} item - The property item object containing value and priority. + * @param {Map} longhandProperties - The map of longhand properties to update. + */ +const updateLonghandProperties = (property, item, longhandProperties) => { + if (longhandProperties.has(property)) { + const { priority: longhandPriority } = longhandProperties.get(property); + if (!longhandPriority) { + longhandProperties.delete(property); + longhandProperties.set(property, item); + } + } else { + longhandProperties.set(property, item); + } +}; + +/** + * Processes border properties from the borders map, expanding and normalizing them. + * + * @param {Map} borders - The map containing accumulated border properties. + * @param {Object} parseOpt - Options for parsing values. + * @returns {Map} A map of fully processed and normalized border properties. + */ +const processBorderProperties = (borders, parseOpt) => { + const longhandProperties = new Map(); + for (const [property, item] of borders) { + if (shorthandProperties.has(property)) { + const { value, priority } = item; + if (property === border.property) { + const lineItems = border.parse(value, parseOpt); + for (const [key, initialValue] of border.initialValues) { + if (!Object.hasOwn(lineItems, key)) { + lineItems[key] = initialValue; + } + } + for (const lineProperty of Object.keys(lineItems)) { + let namePart, linePart; + const hyphenIndex = lineProperty.indexOf("-"); + if (hyphenIndex !== -1) { + namePart = lineProperty.substring(0, hyphenIndex); + linePart = lineProperty.substring(hyphenIndex + 1); + } else { + // fallback for safety, though lineProperty from border.parse keys + // should have hyphen + namePart = lineProperty; + linePart = ""; + } + const lineValue = lineItems[lineProperty]; + for (const position of borderPositions) { + const longhandProperty = `${namePart}-${position}-${linePart}`; + const longhandItem = createPropertyItem(longhandProperty, lineValue, priority); + updateLonghandProperties(longhandProperty, longhandItem, longhandProperties); + } + } + if (value) { + longhandProperties.set(BORDER_IMAGE, createPropertyItem(BORDER_IMAGE, NONE, priority)); + } + } else { + const shorthandItem = shorthandProperties.get(property); + const parsedItem = shorthandItem.parse(value, parseOpt); + if (Array.isArray(parsedItem)) { + let namePart, linePart; + const hyphenIndex = property.indexOf("-"); + if (hyphenIndex !== -1) { + namePart = property.substring(0, hyphenIndex); + linePart = property.substring(hyphenIndex + 1); + } else { + namePart = property; + } + for (const position of borderPositions) { + const longhandProperty = `${namePart}-${position}-${linePart}`; + const longhandValue = getPositionValue(parsedItem, position); + const longhandItem = createPropertyItem(longhandProperty, longhandValue, priority); + updateLonghandProperties(longhandProperty, longhandItem, longhandProperties); + } + } else if (parsedItem) { + for (const [key, initialValue] of shorthandItem.initialValues) { + if (!Object.hasOwn(parsedItem, key)) { + parsedItem[key] = initialValue; + } + } + for (const longhandProperty of Object.keys(parsedItem)) { + const longhandValue = parsedItem[longhandProperty]; + const longhandItem = createPropertyItem(longhandProperty, longhandValue, priority); + updateLonghandProperties(longhandProperty, longhandItem, longhandProperties); + } + } + } + } else if (longhandProperties.has(property)) { + const { priority } = longhandProperties.get(property); + if (!priority) { + longhandProperties.delete(property); + longhandProperties.set(property, item); + } + } else { + longhandProperties.set(property, item); + } + } + const borderItems = prepareBorderShorthands(longhandProperties); + return borderItems; +}; + +/** + * Normalize and prepare CSS properties, handling shorthands and longhands. + * + * @param {Map} properties - The initial map of properties. + * @param {Object} [opt={}] - Parsing options. + * @returns {Map} The normalized map of properties. + */ +const prepareProperties = (properties, opt = {}) => { + const { globalObject, options } = opt; + const parseOpt = { + globalObject, + options + }; + const parsedProperties = new Map(); + const shorthands = new Map(); + const borders = new Map(); + let hasPrecedingBackground = false; + for (const [property, item] of properties) { + const { value, priority } = item; + const { logicalPropertyGroup: shorthandProperty } = propertyDefinitions.get(property) ?? {}; + if (borderProperties.has(property)) { + borders.set(property, { property, value, priority }); + } else if (shorthandProperties.has(shorthandProperty)) { + if (!shorthands.has(shorthandProperty)) { + shorthands.set(shorthandProperty, new Map()); + } + const longhandItems = shorthands.get(shorthandProperty); + if (longhandItems.size) { + const firstPropertyKey = longhandItems.keys().next().value; + const { priority: firstPropertyPriority } = longhandItems.get(firstPropertyKey); + if (priority === firstPropertyPriority) { + longhandItems.set(property, { property, value, priority }); + shorthands.set(shorthandProperty, longhandItems); + } else { + parsedProperties.delete(shorthandProperty); + } + } else { + longhandItems.set(property, { property, value, priority }); + shorthands.set(shorthandProperty, longhandItems); + } + parsedProperties.set(property, item); + } else if (shorthandProperties.has(property)) { + const shorthandItem = shorthandProperties.get(property); + const parsedValues = shorthandItem.parse(value, parseOpt); + let omitShorthandProperty = false; + if (Array.isArray(parsedValues)) { + const [parsedValue] = parsedValues; + if (typeof parsedValue === "string") { + for (const [longhandProperty, longhandItem] of shorthandItem.shorthandFor) { + if (!priority && properties.has(longhandProperty)) { + const { priority: longhandPriority } = properties.get(longhandProperty); + if (longhandPriority) { + omitShorthandProperty = true; + continue; + } + } + const { position } = longhandItem; + const longhandValue = getPositionValue([parsedValue], position); + parsedProperties.set( + longhandProperty, + createPropertyItem(longhandProperty, longhandValue, priority) + ); + } + } else if (parsedValue) { + for (const longhandProperty of Object.keys(parsedValue)) { + const longhandValue = parsedValue[longhandProperty]; + parsedProperties.set( + longhandProperty, + createPropertyItem(longhandProperty, longhandValue, priority) + ); + } + } + } else if (parsedValues && typeof parsedValues !== "string") { + for (const longhandProperty of Object.keys(parsedValues)) { + const longhandValue = parsedValues[longhandProperty]; + parsedProperties.set( + longhandProperty, + createPropertyItem(longhandProperty, longhandValue, priority) + ); + } + } + if (!omitShorthandProperty) { + if (property === background.property) { + hasPrecedingBackground = true; + } + parsedProperties.set(property, createPropertyItem(property, value, priority)); + } + } else { + parsedProperties.set(property, createPropertyItem(property, value, priority)); + if (hasPrecedingBackground) { + const { value: shorthandValue, priority: shorthandPriority } = properties.get( + background.property + ); + if ((!shorthandPriority || priority) && !hasVarFunc(shorthandValue)) { + const replacedShorthandValue = replaceBackgroundShorthand( + property, + parsedProperties, + parseOpt + ); + properties.delete(background.property); + properties.set( + background.property, + createPropertyItem(background.property, replacedShorthandValue, shorthandPriority) + ); + } + } + } + } + if (shorthands.size) { + const shorthandItems = processShorthandProperties(shorthands); + for (const [property, item] of shorthandItems) { + parsedProperties.set(property, item); + } + } + if (borders.size) { + const borderItems = processBorderProperties(borders, parseOpt); + for (const [property, item] of borderItems) { + parsedProperties.set(property, item); + } + } + return parsedProperties; +}; + +/** + * Cleans up redundancy in border properties by removing longhands that are covered by shorthands. + * + * @param {Map} properties - The map of properties to normalize. + * @returns {Map} The normalized properties map. + */ +const normalizeProperties = (properties) => { + if (properties.has(border.property)) { + for (const line of borderLines) { + properties.delete(`${border.property}-${line}`); + } + for (const position of borderPositions) { + properties.delete(`${border.property}-${position}`); + for (const line of borderLines) { + properties.delete(`${border.property}-${position}-${line}`); + } + } + properties.delete(`${border.property}-image`); + } + for (const line of borderLines) { + const lineProperty = `${border.property}-${line}`; + if (properties.has(lineProperty)) { + for (const position of borderPositions) { + const positionProperty = `${border.property}-${position}`; + const longhandProperty = `${border.property}-${position}-${line}`; + properties.delete(positionProperty); + properties.delete(longhandProperty); + } + } + } + for (const position of borderPositions) { + const positionProperty = `${border.property}-${position}`; + if (properties.has(positionProperty)) { + const longhandProperties = []; + for (const line of borderLines) { + const longhandProperty = `${border.property}-${position}-${line}`; + longhandProperties.push(longhandProperty); + } + if (longhandProperties.length === 3) { + for (const longhandProperty of longhandProperties) { + properties.delete(longhandProperty); + } + } else { + properties.delete(positionProperty); + } + } + } + return properties; +}; + +module.exports = { + borderProperties, + getPositionValue, + normalizeProperties, + prepareBorderProperties, + prepareProperties, + shorthandProperties +}; |
