- web/: Vue 3 + Vite + UnoCSS + Pinia, dark tactical theme (amber/#0d1117) - AppNav, ListingCard, SearchView with filters/sort, composables (useSnipeMode, useKonamiCode, useMotion), Pinia search store - Steal shimmer, auction countdown, Snipe Mode easter egg all native in Vue - docker/web/: nginx + multi-stage Dockerfile (node build → nginx serve) - compose.yml: api (8510) + web (8509) services - Dockerfile CMD updated to uvicorn for upcoming FastAPI layer - Clean build: 0 TS errors, 380 modules
613 lines
19 KiB
JavaScript
613 lines
19 KiB
JavaScript
/**
|
|
* This is a fork from the CSS Style Declaration part of
|
|
* https://github.com/NV/CSSOM
|
|
*/
|
|
"use strict";
|
|
|
|
const propertyDescriptors = require("./generated/propertyDescriptors");
|
|
const {
|
|
borderProperties,
|
|
getPositionValue,
|
|
normalizeProperties,
|
|
prepareBorderProperties,
|
|
prepareProperties,
|
|
shorthandProperties
|
|
} = require("./normalize");
|
|
const { hasVarFunc, isGlobalKeyword, parseCSS, parsePropertyValue, prepareValue } = require("./parsers");
|
|
const { asciiLowercase } = require("./utils/strings");
|
|
|
|
/**
|
|
* @see https://drafts.csswg.org/cssom/#the-cssstyledeclaration-interface
|
|
*/
|
|
class CSSStyleDeclaration {
|
|
/**
|
|
* Creates a new CSSStyleDeclaration instance.
|
|
*
|
|
* @param {Function} [onChangeCallback] - Callback triggered when style changes.
|
|
* @param {object} [opt] - Options.
|
|
* @param {object} [opt.context] - The context object (Window, Element, or CSSRule).
|
|
*/
|
|
constructor(onChangeCallback, { context } = {}) {
|
|
// Internals for jsdom
|
|
this._global = globalThis;
|
|
this._onChange = onChangeCallback;
|
|
|
|
// Internals for CSS declaration block
|
|
// @see https://drafts.csswg.org/cssom/#css-declaration-blocks
|
|
this._computed = false;
|
|
this._ownerNode = null;
|
|
this._parentRule = null;
|
|
this._readonly = false;
|
|
this._updating = false;
|
|
|
|
// Other internals
|
|
this._length = 0;
|
|
this._propertyIndices = new Map();
|
|
this._priorities = new Map();
|
|
this._values = new Map();
|
|
|
|
if (context) {
|
|
if (typeof context.getComputedStyle === "function") {
|
|
this._global = context;
|
|
this._computed = true;
|
|
// FIXME: The `_readonly` flag should initially be `false` to be editable,
|
|
// but should eventually be set to `true`.
|
|
// this._readonly = true;
|
|
} else if (context.nodeType === 1 && Object.hasOwn(context, "style")) {
|
|
this._global = context.ownerDocument.defaultView;
|
|
this._ownerNode = context;
|
|
} else if (Object.hasOwn(context, "parentRule")) {
|
|
this._parentRule = context;
|
|
// Find Window from the owner node of the StyleSheet.
|
|
const window = context?.parentStyleSheet?.ownerNode?.ownerDocument?.defaultView;
|
|
if (window) {
|
|
this._global = window;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the textual representation of the declaration block.
|
|
*
|
|
* @returns {string} The serialized CSS text.
|
|
*/
|
|
get cssText() {
|
|
if (this._computed) {
|
|
return "";
|
|
}
|
|
const properties = new Map();
|
|
for (let i = 0; i < this._length; i++) {
|
|
const property = this[i];
|
|
const value = this.getPropertyValue(property);
|
|
const priority = this._priorities.get(property) ?? "";
|
|
if (shorthandProperties.has(property)) {
|
|
const { shorthandFor } = shorthandProperties.get(property);
|
|
for (const [longhand] of shorthandFor) {
|
|
if (priority || !this._priorities.get(longhand)) {
|
|
properties.delete(longhand);
|
|
}
|
|
}
|
|
}
|
|
properties.set(property, { property, value, priority });
|
|
}
|
|
const normalizedProperties = normalizeProperties(properties);
|
|
const parts = [];
|
|
for (const { property, value, priority } of normalizedProperties.values()) {
|
|
if (priority) {
|
|
parts.push(`${property}: ${value} !${priority};`);
|
|
} else {
|
|
parts.push(`${property}: ${value};`);
|
|
}
|
|
}
|
|
return parts.join(" ");
|
|
}
|
|
|
|
/**
|
|
* Sets the textual representation of the declaration block.
|
|
* This clears all existing properties and parses the new CSS text.
|
|
*
|
|
* @param {string} text - The new CSS text.
|
|
*/
|
|
set cssText(text) {
|
|
if (this._readonly) {
|
|
const msg = "cssText can not be modified.";
|
|
const name = "NoModificationAllowedError";
|
|
throw new this._global.DOMException(msg, name);
|
|
}
|
|
this._clearIndexedProperties();
|
|
this._values.clear();
|
|
this._priorities.clear();
|
|
if (this._parentRule || (this._ownerNode && this._updating)) {
|
|
return;
|
|
}
|
|
try {
|
|
this._updating = true;
|
|
const valueObj = parseCSS(text, { context: "declarationList", parseValue: false });
|
|
if (valueObj?.children) {
|
|
const properties = new Map();
|
|
let shouldSkipNext = false;
|
|
for (const item of valueObj.children) {
|
|
if (item.type === "Atrule") {
|
|
continue;
|
|
}
|
|
if (item.type === "Rule") {
|
|
shouldSkipNext = true;
|
|
continue;
|
|
}
|
|
if (shouldSkipNext === true) {
|
|
shouldSkipNext = false;
|
|
continue;
|
|
}
|
|
const {
|
|
important,
|
|
property,
|
|
value: { value }
|
|
} = item;
|
|
if (typeof property === "string" && typeof value === "string") {
|
|
const priority = important ? "important" : "";
|
|
const isCustomProperty = property.startsWith("--");
|
|
if (isCustomProperty || hasVarFunc(value)) {
|
|
if (properties.has(property)) {
|
|
const { priority: itemPriority } = properties.get(property);
|
|
if (!itemPriority) {
|
|
properties.set(property, { property, value, priority });
|
|
}
|
|
} else {
|
|
properties.set(property, { property, value, priority });
|
|
}
|
|
} else {
|
|
const parsedValue = parsePropertyValue(property, value);
|
|
if (parsedValue) {
|
|
if (properties.has(property)) {
|
|
const { priority: itemPriority } = properties.get(property);
|
|
if (!itemPriority) {
|
|
properties.set(property, { property, value, priority });
|
|
}
|
|
} else {
|
|
properties.set(property, { property, value, priority });
|
|
}
|
|
} else {
|
|
this.removeProperty(property);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
const parsedProperties = prepareProperties(properties);
|
|
for (const [property, item] of parsedProperties) {
|
|
const { priority, value } = item;
|
|
this._priorities.set(property, priority);
|
|
this.setProperty(property, value, priority);
|
|
}
|
|
}
|
|
} catch {
|
|
return;
|
|
} finally {
|
|
this._updating = false;
|
|
}
|
|
if (this._onChange) {
|
|
this._onChange(this.cssText);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Returns the number of properties in the declaration block.
|
|
*
|
|
* @returns {number} The property count.
|
|
*/
|
|
get length() {
|
|
return this._length;
|
|
}
|
|
|
|
/**
|
|
* Returns the CSSRule that is the parent of this declaration block.
|
|
*
|
|
* @returns {object|null} The parent CSSRule or null.
|
|
*/
|
|
get parentRule() {
|
|
return this._parentRule;
|
|
}
|
|
|
|
/**
|
|
* Alias for the "float" property.
|
|
*
|
|
* @returns {string} The value of the "float" property.
|
|
*/
|
|
get cssFloat() {
|
|
return this.getPropertyValue("float");
|
|
}
|
|
|
|
/**
|
|
* Sets the "float" property.
|
|
*
|
|
* @param {string} value - The new value for "float".
|
|
*/
|
|
set cssFloat(value) {
|
|
this._setProperty("float", value);
|
|
}
|
|
|
|
/**
|
|
* Returns the priority of the specified property (e.g. "important").
|
|
*
|
|
* @param {string} property - The property name.
|
|
* @returns {string} The priority string, or empty string if not set.
|
|
*/
|
|
getPropertyPriority(property) {
|
|
return this._priorities.get(property) || "";
|
|
}
|
|
|
|
/**
|
|
* Returns the value of the specified property.
|
|
*
|
|
* @param {string} property - The property name.
|
|
* @returns {string} The property value, or empty string if not set.
|
|
*/
|
|
getPropertyValue(property) {
|
|
if (this._values.has(property)) {
|
|
return this._values.get(property).toString();
|
|
}
|
|
return "";
|
|
}
|
|
|
|
/**
|
|
* Returns the property name at the specified index.
|
|
*
|
|
* @param {...number} args - The index (only the first argument is used).
|
|
* @returns {string} The property name, or empty string if index is invalid.
|
|
*/
|
|
item(...args) {
|
|
if (!args.length) {
|
|
const msg = "1 argument required, but only 0 present.";
|
|
throw new this._global.TypeError(msg);
|
|
}
|
|
const [value] = args;
|
|
const index = parseInt(value);
|
|
if (Number.isNaN(index) || index < 0 || index >= this._length) {
|
|
return "";
|
|
}
|
|
return this[index];
|
|
}
|
|
|
|
/**
|
|
* Removes the specified property from the declaration block.
|
|
*
|
|
* @param {string} property - The property name to remove.
|
|
* @returns {string} The value of the removed property.
|
|
*/
|
|
removeProperty(property) {
|
|
if (this._readonly) {
|
|
const msg = `Property ${property} can not be modified.`;
|
|
const name = "NoModificationAllowedError";
|
|
throw new this._global.DOMException(msg, name);
|
|
}
|
|
if (!this._values.has(property)) {
|
|
return "";
|
|
}
|
|
const prevValue = this._values.get(property);
|
|
this._values.delete(property);
|
|
this._priorities.delete(property);
|
|
const index = this._getIndexOf(property);
|
|
if (index >= 0) {
|
|
this._removeIndexedProperty(index);
|
|
if (this._onChange) {
|
|
this._onChange(this.cssText);
|
|
}
|
|
}
|
|
return prevValue;
|
|
}
|
|
|
|
/**
|
|
* Sets a property value with an optional priority.
|
|
*
|
|
* @param {string} property - The property name.
|
|
* @param {string} value - The property value.
|
|
* @param {string} [priority=""] - The priority (e.g. "important").
|
|
*/
|
|
setProperty(property, value, priority = "") {
|
|
if (this._readonly) {
|
|
const msg = `Property ${property} can not be modified.`;
|
|
const name = "NoModificationAllowedError";
|
|
throw new this._global.DOMException(msg, name);
|
|
}
|
|
value = prepareValue(value);
|
|
if (value === "") {
|
|
if (Object.hasOwn(propertyDescriptors, property)) {
|
|
// TODO: Refactor handlers to not require `.call()`.
|
|
propertyDescriptors[property].set.call(this, value);
|
|
}
|
|
this.removeProperty(property);
|
|
return;
|
|
}
|
|
// Custom property
|
|
if (property.startsWith("--")) {
|
|
this._setProperty(property, value, priority);
|
|
return;
|
|
}
|
|
property = asciiLowercase(property);
|
|
if (!Object.hasOwn(propertyDescriptors, property)) {
|
|
return;
|
|
}
|
|
if (priority) {
|
|
this._priorities.set(property, priority);
|
|
} else {
|
|
this._priorities.delete(property);
|
|
}
|
|
propertyDescriptors[property].set.call(this, value);
|
|
}
|
|
|
|
/**
|
|
* Clears all indexed properties, properties indices and resets length to 0.
|
|
*
|
|
* @private
|
|
*/
|
|
_clearIndexedProperties() {
|
|
this._propertyIndices.clear();
|
|
for (let i = 0; i < this._length; i++) {
|
|
delete this[i];
|
|
}
|
|
this._length = 0;
|
|
}
|
|
|
|
/**
|
|
* Removes an indexed property at the specified index, shifts others, and updates indices.
|
|
*
|
|
* @private
|
|
* @param {number} index - The index of the property to remove.
|
|
*/
|
|
_removeIndexedProperty(index) {
|
|
this._propertyIndices.delete(this[index]);
|
|
for (let i = index; i < this._length - 1; i++) {
|
|
const property = this[i + 1];
|
|
this[i] = property;
|
|
this._propertyIndices.set(property, i);
|
|
}
|
|
delete this[this._length - 1];
|
|
this._length--;
|
|
}
|
|
|
|
/**
|
|
* Returns the index of the specified property.
|
|
*
|
|
* @private
|
|
* @param {string} property - The property name to search for.
|
|
* @returns {number} The index of the property, or -1 if not found.
|
|
*/
|
|
_getIndexOf(property) {
|
|
return this._propertyIndices.get(property) ?? -1;
|
|
}
|
|
|
|
/**
|
|
* Sets a property and update indices.
|
|
*
|
|
* @private
|
|
* @param {string} property - The property name.
|
|
* @param {string} value - The property value.
|
|
* @param {string} priority - The priority.
|
|
*/
|
|
_setProperty(property, value, priority) {
|
|
if (typeof value !== "string") {
|
|
return;
|
|
}
|
|
if (value === "") {
|
|
this.removeProperty(property);
|
|
return;
|
|
}
|
|
let originalText = "";
|
|
if (this._onChange && !this._updating) {
|
|
originalText = this.cssText;
|
|
}
|
|
if (!this._values.has(property)) {
|
|
// New property.
|
|
this[this._length] = property;
|
|
this._propertyIndices.set(property, this._length);
|
|
this._length++;
|
|
}
|
|
if (priority === "important") {
|
|
this._priorities.set(property, priority);
|
|
} else {
|
|
this._priorities.delete(property);
|
|
}
|
|
this._values.set(property, value);
|
|
if (this._onChange && !this._updating && this.cssText !== originalText) {
|
|
this._onChange(this.cssText);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to handle border property expansion.
|
|
*
|
|
* @private
|
|
* @param {string} property - The property name (e.g. "border").
|
|
* @param {object|Array|string} value - The value to set.
|
|
* @param {string} priority - The priority.
|
|
*/
|
|
_borderSetter(property, value, priority) {
|
|
const properties = new Map();
|
|
if (typeof priority !== "string") {
|
|
priority = this._priorities.get(property) ?? "";
|
|
}
|
|
if (property === "border") {
|
|
properties.set(property, { propery: property, value, priority });
|
|
} else {
|
|
for (let i = 0; i < this._length; i++) {
|
|
const itemProperty = this[i];
|
|
if (borderProperties.has(itemProperty)) {
|
|
const itemValue = this.getPropertyValue(itemProperty);
|
|
const longhandPriority = this._priorities.get(itemProperty) ?? "";
|
|
let itemPriority = longhandPriority;
|
|
if (itemProperty === property) {
|
|
itemPriority = priority;
|
|
}
|
|
properties.set(itemProperty, {
|
|
property: itemProperty,
|
|
value: itemValue,
|
|
priority: itemPriority
|
|
});
|
|
}
|
|
}
|
|
}
|
|
const parsedProperties = prepareBorderProperties(property, value, priority, properties);
|
|
for (const [itemProperty, item] of parsedProperties) {
|
|
const { priority: itemPriority, value: itemValue } = item;
|
|
this._setProperty(itemProperty, itemValue, itemPriority);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to handle flexbox shorthand expansion.
|
|
*
|
|
* @private
|
|
* @param {string} property - The property name.
|
|
* @param {string} value - The property value.
|
|
* @param {string} priority - The priority.
|
|
* @param {string} shorthandProperty - The shorthand property name.
|
|
*/
|
|
_flexBoxSetter(property, value, priority, shorthandProperty) {
|
|
if (!shorthandProperty || !shorthandProperties.has(shorthandProperty)) {
|
|
return;
|
|
}
|
|
const shorthandPriority = this._priorities.get(shorthandProperty);
|
|
this.removeProperty(shorthandProperty);
|
|
if (typeof priority !== "string") {
|
|
priority = this._priorities.get(property) ?? "";
|
|
}
|
|
this.removeProperty(property);
|
|
if (shorthandPriority && priority) {
|
|
this._setProperty(property, value);
|
|
} else {
|
|
this._setProperty(property, value, priority);
|
|
}
|
|
if (value && !hasVarFunc(value)) {
|
|
const longhandValues = [];
|
|
const shorthandItem = shorthandProperties.get(shorthandProperty);
|
|
let hasGlobalKeyword = false;
|
|
for (const [longhandProperty] of shorthandItem.shorthandFor) {
|
|
if (longhandProperty === property) {
|
|
if (isGlobalKeyword(value)) {
|
|
hasGlobalKeyword = true;
|
|
}
|
|
longhandValues.push(value);
|
|
} else {
|
|
const longhandValue = this.getPropertyValue(longhandProperty);
|
|
const longhandPriority = this._priorities.get(longhandProperty) ?? "";
|
|
if (!longhandValue || longhandPriority !== priority) {
|
|
break;
|
|
}
|
|
if (isGlobalKeyword(longhandValue)) {
|
|
hasGlobalKeyword = true;
|
|
}
|
|
longhandValues.push(longhandValue);
|
|
}
|
|
}
|
|
if (longhandValues.length === shorthandItem.shorthandFor.size) {
|
|
if (hasGlobalKeyword) {
|
|
const [firstValue, ...restValues] = longhandValues;
|
|
if (restValues.every((val) => val === firstValue)) {
|
|
this._setProperty(shorthandProperty, firstValue, priority);
|
|
}
|
|
} else {
|
|
const parsedValue = shorthandItem.parse(longhandValues.join(" "));
|
|
const shorthandValue = Object.values(parsedValue).join(" ");
|
|
this._setProperty(shorthandProperty, shorthandValue, priority);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to handle position shorthand expansion.
|
|
*
|
|
* @private
|
|
* @param {string} property - The property name.
|
|
* @param {Array|string} value - The property value.
|
|
* @param {string} priority - The priority.
|
|
*/
|
|
_positionShorthandSetter(property, value, priority) {
|
|
if (!shorthandProperties.has(property)) {
|
|
return;
|
|
}
|
|
const shorthandValues = [];
|
|
if (Array.isArray(value)) {
|
|
shorthandValues.push(...value);
|
|
} else if (typeof value === "string") {
|
|
shorthandValues.push(value);
|
|
} else {
|
|
return;
|
|
}
|
|
if (typeof priority !== "string") {
|
|
priority = this._priorities.get(property) ?? "";
|
|
}
|
|
const { position, shorthandFor } = shorthandProperties.get(property);
|
|
let hasPriority = false;
|
|
for (const [longhandProperty, longhandItem] of shorthandFor) {
|
|
const { position: longhandPosition } = longhandItem;
|
|
const longhandValue = getPositionValue(shorthandValues, longhandPosition);
|
|
if (priority) {
|
|
this._setProperty(longhandProperty, longhandValue, priority);
|
|
} else {
|
|
const longhandPriority = this._priorities.get(longhandProperty) ?? "";
|
|
if (longhandPriority) {
|
|
hasPriority = true;
|
|
} else {
|
|
this._setProperty(longhandProperty, longhandValue, priority);
|
|
}
|
|
}
|
|
}
|
|
if (hasPriority) {
|
|
this.removeProperty(property);
|
|
} else {
|
|
const shorthandValue = getPositionValue(shorthandValues, position);
|
|
this._setProperty(property, shorthandValue, priority);
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Helper to handle position longhand updates affecting shorthands.
|
|
*
|
|
* @private
|
|
* @param {string} property - The property name.
|
|
* @param {string} value - The property value.
|
|
* @param {string} priority - The priority.
|
|
* @param {string} shorthandProperty - The shorthand property name.
|
|
*/
|
|
_positionLonghandSetter(property, value, priority, shorthandProperty) {
|
|
if (!shorthandProperty || !shorthandProperties.has(shorthandProperty)) {
|
|
return;
|
|
}
|
|
const shorthandPriority = this._priorities.get(shorthandProperty);
|
|
this.removeProperty(shorthandProperty);
|
|
if (typeof priority !== "string") {
|
|
priority = this._priorities.get(property) ?? "";
|
|
}
|
|
this.removeProperty(property);
|
|
if (shorthandPriority && priority) {
|
|
this._setProperty(property, value);
|
|
} else {
|
|
this._setProperty(property, value, priority);
|
|
}
|
|
if (value && !hasVarFunc(value)) {
|
|
const longhandValues = [];
|
|
const { shorthandFor, position: shorthandPosition } = shorthandProperties.get(shorthandProperty);
|
|
for (const [longhandProperty] of shorthandFor) {
|
|
const longhandValue = this.getPropertyValue(longhandProperty);
|
|
const longhandPriority = this._priorities.get(longhandProperty) ?? "";
|
|
if (!longhandValue || longhandPriority !== priority) {
|
|
return;
|
|
}
|
|
longhandValues.push(longhandValue);
|
|
}
|
|
if (longhandValues.length === shorthandFor.size) {
|
|
const replacedValue = getPositionValue(longhandValues, shorthandPosition);
|
|
this._setProperty(shorthandProperty, replacedValue);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// TODO: Remove once the CSSStyleDeclaration is fully spec-compliant.
|
|
// @see https://github.com/jsdom/cssstyle/issues/255#issuecomment-3630183207
|
|
Object.defineProperties(CSSStyleDeclaration.prototype, propertyDescriptors);
|
|
|
|
module.exports = {
|
|
CSSStyleDeclaration
|
|
};
|