Files
ANDJJJJJJ/server/node_modules/cssstyle/lib/normalize.js

1575 lines
53 KiB
JavaScript

"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
};