UNPKG

css-typed-om-polyfill

Version:

A JavaScript polyfill for the CSS Typed Object Model (Level 1)

1,047 lines (991 loc) 63.5 kB
(function () { 'use strict' console.log('CSS Typed OM Polyfill: Initializing...') // --- Helper: Simple unit definitions --- const LENGTH_UNITS = ['px', 'em', 'rem', 'vw', 'vh', 'vmin', 'vmax', 'cm', 'mm', 'in', 'pt', 'pc', 'ch', 'ex', 'cap', 'ic', 'lh', 'rlh', 'q'] const ANGLE_UNITS = ['deg', 'rad', 'grad', 'turn'] const TIME_UNITS = ['s', 'ms'] const FREQUENCY_UNITS = ['hz', 'khz'] const RESOLUTION_UNITS = ['dpi', 'dpcm', 'dppx'] const FLEX_UNITS = ['fr'] const PERCENTAGE_UNIT = ['%'] const NUMBER_UNIT_TYPE = 'number' const ALL_UNITS = [ ...LENGTH_UNITS, ...ANGLE_UNITS, ...TIME_UNITS, ...FREQUENCY_UNITS, ...RESOLUTION_UNITS, ...FLEX_UNITS, PERCENTAGE_UNIT[0] ] function getUnitCategory(unit) { if (typeof unit !== 'string') return 'unknown' unit = unit.toLowerCase() if (LENGTH_UNITS.includes(unit)) return 'length' if (ANGLE_UNITS.includes(unit)) return 'angle' if (TIME_UNITS.includes(unit)) return 'time' if (FREQUENCY_UNITS.includes(unit)) return 'frequency' if (RESOLUTION_UNITS.includes(unit)) return 'resolution' if (FLEX_UNITS.includes(unit)) return 'flex' if (PERCENTAGE_UNIT.includes(unit)) return 'percent' if (unit === NUMBER_UNIT_TYPE) return NUMBER_UNIT_TYPE return 'unknown' } const conversionRates = { time: { ms: 1, s: 1000 }, angle: { deg: 1, rad: 180 / Math.PI, grad: 400 / 360, turn: 360 }, } // --- CSSStyleValue (Base Class) --- class CSSStyleValue { constructor() { if (this.constructor === CSSStyleValue) { throw new TypeError("Illegal constructor: CSSStyleValue is an abstract base class.") } } } // --- Simple Numeric Value Parser --- function parseSimpleCssNumericValue(text) { text = text.trim() const unitMatch = text.match(/^(-?(?:\d*\.\d+|\d+\.?\d*))(%|[a-zA-Z]+)?$/) if (unitMatch) { const value = parseFloat(unitMatch[1]) if (!isFinite(value)) return null const unit = unitMatch[2] ? unitMatch[2].toLowerCase() : undefined if (unit === undefined) return new CSSNumericValue(value, NUMBER_UNIT_TYPE) if (unit === '%') return new CSSUnitValue(value, '%') if (ALL_UNITS.includes(unit)) return new CSSUnitValue(value, unit) return null // Unknown unit } if (/^(-?(?:\d*\.\d+|\d+\.?\d*))$/.test(text)) { const value = parseFloat(text) if (isFinite(value)) return new CSSNumericValue(value, NUMBER_UNIT_TYPE) } return null } // --- Parenthesis Matcher --- function findMatchingParen(str, startIndex = 0) { let level = 0 if (str[startIndex] !== '(') return -1 for (let i = startIndex; i < str.length; i++) { if (str[i] === '(') level++ else if (str[i] === ')') { level-- if (level === 0) return i } } return -1 } // --- Function Argument Parser --- function parseFunctionArguments(argsString, functionName = 'function') { argsString = argsString.trim() if (argsString === '') return [] const args = [] let currentArg = '' let parenDepth = 0 for (let i = 0; i < argsString.length; i++) { const char = argsString[i] if (char === '(') { parenDepth++ currentArg += char } else if (char === ')') { currentArg += char parenDepth = Math.max(0, parenDepth - 1) } else if (char === ',' && parenDepth === 0) { const argTrimmed = currentArg.trim() if (argTrimmed === '') throw new Error(`Empty argument found in ${functionName}()`) args.push(parseCssMathExpression(argTrimmed)) // Use MATH parser currentArg = '' } else { currentArg += char } } const lastArgTrimmed = currentArg.trim() if (lastArgTrimmed === '') { if (argsString.endsWith(',')) throw new Error(`Trailing comma in ${functionName}()`) if (args.length === 0) throw new Error(`No arguments provided to ${functionName}()`) } else { args.push(parseCssMathExpression(lastArgTrimmed)) // Use MATH parser } return args } // --- Recursive CSS Math Expression Parser --- function parseCssMathExpression(expression) { expression = expression.trim() // 1. Outermost Parentheses if (expression.startsWith('(') && findMatchingParen(expression, 0) === expression.length - 1) { return parseCssMathExpression(expression.slice(1, -1)) } // 2. Addition/Subtraction (right-to-left) let parenLevel = 0 for (let i = expression.length - 1; i >= 0; i--) { const char = expression[i] if (char === ')') parenLevel++ else if (char === '(') parenLevel-- else if (parenLevel === 0 && (char === '+' || char === '-')) { const prevChar = expression[i - 1] const nextChar = expression[i + 1] // CSS requires spaces around binary +/- for calc() if (/\s/.test(prevChar) || /\s/.test(nextChar)) { const left = expression.slice(0, i) const right = expression.slice(i + 1) if (left.trim() !== '' && right.trim() !== '') { // Check it's not unary following another operator/paren/start let prevNonSpaceIndex = i - 1 while (prevNonSpaceIndex >= 0 && /\s/.test(expression[prevNonSpaceIndex])) prevNonSpaceIndex-- if (prevNonSpaceIndex >= 0 && !['(', '+', '-', '*', '/'].includes(expression[prevNonSpaceIndex])) { // Binary operator confirmed const leftVal = parseCssMathExpression(left) // Might be VarRef const rightVal = parseCssMathExpression(right) // Might be VarRef if (char === '+') return leftVal.add(rightVal) // Use .add() if (char === '-') return leftVal.sub(rightVal) // Use .sub() } } else { throw new Error(`Invalid syntax: Missing operand around operator '${char}' in "${expression}"`) } } // else: likely unary or invalid syntax like "10px-5px" } } // 3. Multiplication/Division (right-to-left) parenLevel = 0 for (let i = expression.length - 1; i >= 0; i--) { const char = expression[i] if (char === ')') parenLevel++ else if (char === '(') parenLevel-- else if (parenLevel === 0 && (char === '*' || char === '/')) { const left = expression.slice(0, i) const right = expression.slice(i + 1) if (left.trim() === '' || right.trim() === '') { throw new Error(`Invalid syntax: Missing operand around operator '${char}' in "${expression}"`) } const leftVal = parseCssMathExpression(left) const rightVal = parseCssMathExpression(right) if (char === '*') return leftVal.mul(rightVal) // Use .mul() if (char === '/') return leftVal.div(rightVal) // Use .div() } } // 4. Unary Minus/Plus if (expression.startsWith('-')) { const operand = parseCssMathExpression(expression.slice(1)) return operand.negate() } if (expression.startsWith('+')) { return parseCssMathExpression(expression.slice(1)) } // 5. Functions (min, max, var, etc. as terms within calc) const funcMatch = expression.match(/^([a-zA-Z-]+)\((.*)\)$/i) if (funcMatch) { const funcName = funcMatch[1].toLowerCase() const funcContent = funcMatch[2] switch (funcName) { case 'min': case 'max': try { const args = parseFunctionArguments(funcContent, funcName) args.forEach(arg => { if (!(arg instanceof CSSNumericValue || arg instanceof CSSVariableReferenceValue)) throw new Error(`Invalid argument type ${arg?.constructor?.name}`) }) if (args.length === 0) throw new Error(`No arguments for ${funcName}`) return funcName === 'min' ? new CSSMathMin(...args) : new CSSMathMax(...args) } catch (e) { throw new Error(`Failed to parse ${funcName} args: ${e.message}`) } case 'clamp': throw new Error(`clamp() not supported`) case 'var': const varParts = funcContent.match(/^\s*(--[a-zA-Z0-9_-]+)\s*(?:,\s*(.+)\s*)?$/) if (varParts) { const fallback = varParts[2] ? new CSSUnparsedValue([varParts[2].trim()]) : null return new CSSVariableReferenceValue(varParts[1], fallback) } else { throw new Error(`Invalid var() syntax: "${expression}"`) } case 'calc': return parseCssMathExpression(funcContent) // Nested calc default: throw new Error(`Unsupported function "${funcName}()" in math expression.`) } } // 6. Base Case: Simple numeric value const simpleValue = parseSimpleCssNumericValue(expression) if (simpleValue) return simpleValue // 7. Invalid Term throw new Error(`Invalid or unsupported term in math expression: "${expression}"`) } // --- CSSNumericValue (Base for numbers, units, math) --- class CSSNumericValue extends CSSStyleValue { constructor(value, unitType) { super() this._value = (unitType === NUMBER_UNIT_TYPE) ? value : NaN this._unitType = unitType } type() { const types = { length: 0, angle: 0, time: 0, frequency: 0, resolution: 0, flex: 0, percent: 0 } const key = this._unitType if (key === NUMBER_UNIT_TYPE) { /* all 0 */ } else if (key === 'percent') types.percent = 1 else if (types.hasOwnProperty(key)) types[key] = 1 else if (key !== 'mixed' && key !== 'unknown') console.warn(`CSSNumericValue.type(): Unknown unit type category "${key}"`) return types } _toNumericValue(val, opName) { if (val instanceof CSSNumericValue || val instanceof CSSVariableReferenceValue) return val if (typeof val === 'number') return new CSSNumericValue(val, NUMBER_UNIT_TYPE) if (typeof val === 'string') { try { const parsed = CSSStyleValue.parse('', val) // Use main parser if (parsed instanceof CSSNumericValue || parsed instanceof CSSVariableReferenceValue ) return parsed throw new TypeError(`String "${val}" parsed to non-numeric type "${parsed?.constructor?.name}" for ${opName}.`) } catch (e) { throw new TypeError(`Could not parse string "${val}" for ${opName}: ${e.message}`) } } throw new TypeError(`Cannot use value of type ${typeof val} in numeric ${opName}.`) } // --- Arithmetic Methods --- // These methods handle CSSVariableReferenceValue by creating Math objects. add(...values) { let result = this for (const rawVal of values) { const other = this._toNumericValue(rawVal, 'add') if (result instanceof CSSMathValue || other instanceof CSSMathValue) { const left = (result instanceof CSSMathSum || result instanceof CSSMathProduct) ? result.values : [result] const right = (other instanceof CSSMathSum || other instanceof CSSMathProduct) ? other.values : [other] result = new CSSMathSum([...left, ...right]) continue } if (result instanceof CSSUnitValue && other instanceof CSSUnitValue) { if (result.unit === other.unit) result = new CSSUnitValue(result.value + other.value, result.unit) else { const cat1 = getUnitCategory(result.unit) const cat2 = getUnitCategory(other.unit) const conv = ['length', 'angle', 'time', 'frequency', 'resolution'] if (cat1 !== 'unknown' && cat1 === cat2 && conv.includes(cat1)) { try { const convOther = other.to(result.unit) result = new CSSUnitValue(result.value + convOther.value, result.unit) } catch (e) { result = new CSSMathSum([result, other]) } } else result = new CSSMathSum([result, other]) } } else result = new CSSMathSum([result, other]) } return result } sub(...values) { let result = this for (const rawVal of values) { const other = this._toNumericValue(rawVal, 'sub') // Use add(negate(other)) strategy result = result.add(other.negate()) } return result } mul(...values) { let result = this for (const rawVal of values) { const other = this._toNumericValue(rawVal, 'mul') if (result instanceof CSSMathValue || other instanceof CSSMathValue) { const left = (result instanceof CSSMathProduct) ? result.values : [result] const right = (other instanceof CSSMathProduct) ? other.values : [other] result = new CSSMathProduct([...left, ...right]) continue } const typeR = result.type() const typeO = other.type() const isRUnitless = Object.values(typeR).every(v => v === 0) const isOUnitless = Object.values(typeO).every(v => v === 0) let direct = null if (result instanceof CSSUnitValue && isOUnitless && other instanceof CSSNumericValue) direct = new CSSUnitValue(result.value * other._value, result.unit) else if (other instanceof CSSUnitValue && isRUnitless && result instanceof CSSNumericValue) direct = new CSSUnitValue(result._value * other.value, other.unit) else if (isRUnitless && isOUnitless && result instanceof CSSNumericValue && other instanceof CSSNumericValue) direct = new CSSNumericValue(result._value * other._value, NUMBER_UNIT_TYPE) if (direct) result = direct else { const left = (result instanceof CSSMathProduct) ? result.values : [result] const right = (other instanceof CSSMathProduct) ? other.values : [other] result = new CSSMathProduct([...left, ...right]) } } return result } div(...values) { let result = this for (const rawVal of values) { const other = this._toNumericValue(rawVal, 'div') // Check for division by zero early if possible (non-var) const isZero = (other instanceof CSSUnitValue && other.value === 0) || (other instanceof CSSNumericValue && other._unitType === NUMBER_UNIT_TYPE && other._value === 0) if (isZero) throw new RangeError("Division by zero in CSS calculation.") // Use mul(invert(other)) strategy result = result.mul(other.invert()) } return result } // --- Structural Operations --- negate() { if (this instanceof CSSMathNegate) return this.value // -(-x) -> x if (this instanceof CSSUnitValue) return new CSSUnitValue(-this.value, this.unit) if (this instanceof CSSNumericValue && this._unitType === NUMBER_UNIT_TYPE) return new CSSNumericValue(-this._value, NUMBER_UNIT_TYPE) return new CSSMathNegate(this) // Wrap others (including VarRef) } invert() { if (this instanceof CSSMathInvert) return this.value // 1/(1/x) -> x // Check for inversion of zero if ((this instanceof CSSUnitValue && this.value === 0) || (this instanceof CSSNumericValue && this._unitType === NUMBER_UNIT_TYPE && this._value === 0)) { throw new RangeError("Division by zero (inversion of 0).") } if (this instanceof CSSNumericValue && this._unitType === NUMBER_UNIT_TYPE) return new CSSNumericValue(1 / this._value, NUMBER_UNIT_TYPE) return new CSSMathInvert(this) // Wrap others (UnitValue, VarRef, Math) } // --- Comparison / Conversion --- min(...values) { const all = [this, ...values].map(v => this._toNumericValue(v, 'min')) return new CSSMathMin(...all) } max(...values) { const all = [this, ...values].map(v => this._toNumericValue(v, 'max')) return new CSSMathMax(...all) } equals(...values) { /* ... Simplified structural check ... */ for (const rawVal of values) { let other if (rawVal instanceof CSSNumericValue || rawVal instanceof CSSVariableReferenceValue) other = rawVal else if (typeof rawVal === 'string' || typeof rawVal === 'number') { try { other = this._toNumericValue(rawVal, 'equals') } catch (e) { return false } } else return false // Cannot compare other types // Crude string comparison (unreliable for math) + zero check if (this.toString() !== other.toString()) { const isThisZero = (this instanceof CSSUnitValue && this.value === 0) || (this instanceof CSSNumericValue && this._unitType === NUMBER_UNIT_TYPE && this._value === 0) const isOtherZero = (other instanceof CSSUnitValue && other.value === 0) || (other instanceof CSSNumericValue && other._unitType === NUMBER_UNIT_TYPE && other._value === 0) if (!(isThisZero && isOtherZero)) return false } } return true } to(targetUnit) { if (!(this instanceof CSSUnitValue)) throw new TypeError(".to() only supported on CSSUnitValue") targetUnit = targetUnit.toLowerCase() if (this.unit === targetUnit) return new CSSUnitValue(this.value, this.unit) const cat1 = getUnitCategory(this.unit) const cat2 = getUnitCategory(targetUnit) if (cat1 === 'unknown' || cat2 === 'unknown' || cat1 !== cat2) throw new TypeError(`Cannot convert from "${this.unit}" to "${targetUnit}". Incompatible categories.`) if (cat1 === NUMBER_UNIT_TYPE || cat2 === NUMBER_UNIT_TYPE || cat1 === 'percent' || cat2 === 'percent') throw new TypeError(`Cannot convert between units and number/percent using .to().`) if (conversionRates[cat1]) { const rates = conversionRates[cat1] if (rates[this.unit] === undefined || rates[targetUnit] === undefined) throw new TypeError(`Conversion between "${this.unit}" and "${targetUnit}" not supported by polyfill rates.`) const valueInBase = this.value * rates[this.unit] const convertedValue = valueInBase / rates[targetUnit] return new CSSUnitValue(convertedValue, targetUnit) } throw new TypeError(`Conversion from "${this.unit}" to "${targetUnit}" (category "${cat1}") is not supported by this polyfill.`) } toSum(...values) { if (values.length > 0) throw new TypeError(".toSum() does not accept arguments.") return (this instanceof CSSMathSum) ? this : new CSSMathSum([this]) } // --- Default toString --- toString() { if (this._unitType === NUMBER_UNIT_TYPE) return String(this._value) console.warn("CSSNumericValue.toString() called on base/unexpected type:", this.constructor.name) return 'calc(?)' } static parse(cssText) { const p = CSSStyleValue.parse('', cssText.trim()) if (p instanceof CSSNumericValue) return p if (p instanceof CSSVariableReferenceValue) throw new TypeError(`Input "${cssText}" parsed as a CSSVariableReferenceValue.`) throw new TypeError(`Could not parse "${cssText}" as a CSSNumericValue (parsed as ${p?.constructor?.name || 'unknown'}).`) } } // --- CSSUnitValue --- class CSSUnitValue extends CSSNumericValue { constructor(value, unit) { if (typeof value !== 'number' || !isFinite(value)) throw new TypeError("Value must be finite number.") if (typeof unit !== 'string' || unit === '') throw new TypeError("Unit must be non-empty string.") const unitLower = unit.toLowerCase() const category = getUnitCategory(unitLower) if (category === NUMBER_UNIT_TYPE) throw new TypeError(`Cannot create CSSUnitValue with unit type number.`) super(value, category) // Pass category to parent if (category === 'unknown') console.warn(`CSSUnitValue: Created value with unknown unit "${unit}".`) this._internalValue = value this._unit = unitLower } get value() { return this._internalValue } set value(v) { if (typeof v !== 'number' || !isFinite(v)) throw new TypeError("Value must be finite number.") this._internalValue = v } get unit() { return this._unit } toString() { return `${this.value}${this.unit}` } } // --- CSSKeywordValue --- class CSSKeywordValue extends CSSStyleValue { constructor(value) { super() if (typeof value !== 'string' || value.trim() === '') throw new TypeError("Keyword value must be non-empty string.") // Relaxed identifier check if (!/^-?[_a-zA-Z]/.test(value) && !['inherit', 'initial', 'unset', 'revert', 'auto', 'none'].includes(value.toLowerCase())) { console.warn(`CSSKeywordValue: Value "${value}" might not be a valid CSS keyword.`) } this._value = value } get value() { return this._value } set value(v) { if (typeof v !== 'string' || v.trim() === '') throw new TypeError("Keyword value must be non-empty string.") this._value = v } toString() { return this.value } } // --- CSSUnparsedValue --- // (Declaration moved up for forward reference) class CSSUnparsedValue extends CSSStyleValue { constructor(members = []) { super() if (!Array.isArray(members)) throw new TypeError("CSSUnparsedValue needs an array.") if (!members.every(m => typeof m === 'string' || m instanceof CSSVariableReferenceValue)) { const invalid = members.find(m => typeof m !== 'string' && !(m instanceof CSSVariableReferenceValue)) throw new TypeError(`CSSUnparsedValue members must be strings or CSSVariableReferenceValue. Found: ${invalid?.constructor?.name || typeof invalid}`) } this._members = Object.freeze([...members]) } [Symbol.iterator]() { return this._members[Symbol.iterator]() } get length() { return this._members.length } item(i) { return this._members[i] } toString() { return this._members.map(m => String(m)).join('') } // Implement array-like readonly properties/methods entries() { return this._members.entries() } forEach(callback, thisArg) { this._members.forEach(callback, thisArg) } keys() { return this._members.keys() } values() { return this._members.values() } } // --- CSSVariableReferenceValue --- class CSSVariableReferenceValue extends CSSStyleValue { // Doesn't inherit CSSNumericValue! constructor(variable, fallback = null) { super() if (typeof variable !== 'string' || !variable.startsWith('--')) throw new TypeError("Variable name must start with '--'.") if (fallback !== null && !(fallback instanceof CSSUnparsedValue)) throw new TypeError("Fallback must be CSSUnparsedValue or null.") this.variable = variable.trim() this.fallback = fallback } toString() { return `var(${this.variable}${this.fallback ? `, ${this.fallback.toString()}` : ''})` } type() { console.warn("CSSVariableReferenceValue.type(): Type is indeterminate.") return {} } // *** FIX: Add delegating arithmetic methods to provide the interface needed by the parser *** _toNumericValue(...values) { return CSSNumericValue.prototype._toNumericValue.call(this, ...values) } add(...values) { return CSSNumericValue.prototype.add.call(this, ...values) } sub(...values) { return CSSNumericValue.prototype.sub.call(this, ...values) } mul(...values) { return CSSNumericValue.prototype.mul.call(this, ...values) } div(...values) { return CSSNumericValue.prototype.div.call(this, ...values) } min(...values) { return CSSNumericValue.prototype.min.call(this, ...values) } max(...values) { return CSSNumericValue.prototype.max.call(this, ...values) } negate() { return CSSNumericValue.prototype.negate.call(this) } invert() { return CSSNumericValue.prototype.invert.call(this) } // Add equals/to/toSum? The parser uses add/sub/mul/div/negate/invert primarily. // Let's skip adding the others unless needed, as VarRef isn't truly numeric itself. } // --- CSSMathValue Base --- class CSSMathValue extends CSSNumericValue { constructor() { super(NaN, 'mixed') if (this.constructor === CSSMathValue) throw new TypeError("CSSMathValue is abstract.") this._operands = [] } get values() { return this._operands } // Helper for formatting operands in toString() _formatOperand(op, context = 'sum') { // context 'sum' or 'product' let opStr = op.toString() let needsParens = false // Parenthesize lower precedence operations, or functions if (context === 'sum' && (op instanceof CSSMathSum)) needsParens = true // e.g. a + (b+c) if (context === 'product' && (op instanceof CSSMathSum)) needsParens = true // e.g. a * (b+c) // Always wrap nested calc/min/max/clamp? Safer. if (opStr.startsWith('calc(') || opStr.startsWith('min(') || opStr.startsWith('max(') || opStr.startsWith('clamp(')) { needsParens = true if (opStr.startsWith('calc(')) opStr = opStr.slice(5, -1).trim() } // Parenthesize negation if it's not a simple negative number/unit if (op instanceof CSSMathNegate && !(op.value instanceof CSSUnitValue || (op.value instanceof CSSNumericValue && op.value._unitType === NUMBER_UNIT_TYPE))) { needsParens = true } // Parenthesize inversion if it's not a simple number inversion if (op instanceof CSSMathInvert && !(op.value instanceof CSSNumericValue && op.value._unitType === NUMBER_UNIT_TYPE)) { needsParens = true } if (needsParens) { // Check again if simple after stripping calc() return (opStr.includes(' ') || opStr.includes('(') || opStr.includes(',')) ? `(${opStr})` : opStr } return opStr } } class CSSMathSum extends CSSMathValue { constructor(operands) { super() if (!Array.isArray(operands) || operands.length === 0) throw new TypeError("CSSMathSum needs operands.") this._operands = Object.freeze(operands.map((op, i) => { if (op instanceof CSSNumericValue || op instanceof CSSVariableReferenceValue) return op throw new TypeError(`CSSMathSum operand ${i + 1} invalid type ${op?.constructor?.name}`) })) } get operator() { return "sum" } toString() { if (this.values.length === 0) return 'calc(0)' let result = '' for (let i = 0; i < this.values.length; i++) { const op = this.values[i] let sign = ' + ' let valueToFormat = op if (op instanceof CSSMathNegate) { sign = ' - ' valueToFormat = op.value } if (i === 0 && sign === ' - ') result += '-' + this._formatOperand(valueToFormat, 'sum') else { if (i > 0) result += sign result += this._formatOperand(valueToFormat, 'sum') } } return `calc(${result})` } // Refined type compatibility check type() { if (this._operands.length === 0) return super.type() let commonType = null let hasVar = false let hasUnknown = false for (const op of this._operands) { let opType if (op instanceof CSSVariableReferenceValue) { hasVar = true opType = {} /* Treat var as empty type for check */ } else if (op instanceof CSSNumericValue) { opType = op.type() } else { hasUnknown = true continue } // Should not happen if constructor validated // Skip check if operand is var() - just note its presence const isOpVar = (op instanceof CSSVariableReferenceValue) if (Object.keys(opType).length === 0 && !isOpVar && !(op instanceof CSSNumericValue && op._unitType === NUMBER_UNIT_TYPE)) { hasUnknown = true continue } if (!commonType && !hasUnknown) { commonType = { ...opType } } // Initialize else if (!hasUnknown) { // Check compatibility with commonType, allowing var() presence const currentKeys = Object.keys(commonType).filter(k => commonType[k] !== 0) const opKeys = Object.keys(opType).filter(k => opType[k] !== 0) const currentIsVarPlaceholder = currentKeys.length === 0 && hasVar // Is commonType just from previous vars? if (isOpVar) continue // Don't check compatibility against var() itself let compatible = false // Check against established non-var commonType const commonCategory = currentKeys.find(k => k !== 'percent') const opCategory = opKeys.find(k => k !== 'percent') if (!commonCategory && !opCategory) compatible = true // number/percent + number/percent else if (commonCategory === 'length' && !opCategory) compatible = true // length + number/percent else if (!commonCategory && opCategory === 'length') compatible = true // number/percent + length else if (commonCategory && commonCategory === opCategory) compatible = true // unit + same unit (potentially + percent) // else: incompatible categories if (compatible) { // Update commonType to reflect mix if needed (e.g., adding percent) if (!commonType.length && opType.length) commonType.length = opType.length if (!commonType.percent && opType.percent) commonType.percent = opType.percent // Update other types similarly if applicable } else { console.error(`CSSMathSum incompatible additive types: {${currentKeys.join()}} + {${opKeys.join()}}. Expr:`, this.toString()) return {} // Incompatible mix } } } if (hasUnknown && !commonType) return {} // Only unknowns if (hasVar) console.warn("CSSMathSum.type(): Type includes var(), result indeterminate.") return commonType || {} // Return combined type (or empty if only vars/unknowns) } } // --- CSSMathProduct --- class CSSMathProduct extends CSSMathValue { constructor(operands) { super() if (!Array.isArray(operands) || operands.length === 0) throw new TypeError("CSSMathProduct needs operands.") this._operands = Object.freeze(operands.map((op, i) => { if (op instanceof CSSNumericValue || op instanceof CSSVariableReferenceValue) return op throw new TypeError(`CSSMathProduct operand ${i + 1} invalid type ${op?.constructor?.name}`) })) } get operator() { return "product" } toString() { const numTerms = [] const denTerms = [] this.values.forEach(op => { let valueToFormat = op let isDen = false if (op instanceof CSSMathInvert) { isDen = true valueToFormat = op.value } const termStr = this._formatOperand(valueToFormat, 'product') // Use product context if (isDen) denTerms.push(termStr) else numTerms.push(termStr) }) const numStr = numTerms.length === 0 ? '1' : numTerms.join(' * ') let result = numStr if (denTerms.length > 0) { const denStr = denTerms.join(' * ') // Wrap denominator if it has spaces or is a function call, indicating complexity or multiple terms const wrapDen = denTerms.length > 1 || denStr.includes(' ') || denStr.includes('(') result += ` / ${wrapDen ? `(${denStr})` : denStr}` } return `calc(${result})` } type() { const combined = { length: 0, angle: 0, time: 0, frequency: 0, resolution: 0, flex: 0, percent: 0 } let hasVar = false let hasUnknown = false this._operands.forEach(op => { if (op instanceof CSSVariableReferenceValue) { hasVar = true return } let effType let isInverted = op instanceof CSSMathInvert const baseVal = isInverted ? op.value : op if (baseVal instanceof CSSVariableReferenceValue) { hasVar = true return } if (!(baseVal instanceof CSSNumericValue)) { hasUnknown = true return } effType = baseVal.type() if (Object.keys(effType).length === 0 && !(baseVal instanceof CSSNumericValue && baseVal._unitType === NUMBER_UNIT_TYPE)) { hasUnknown = true return } for (const key in effType) { if (combined.hasOwnProperty(key)) combined[key] += (isInverted ? -1 : 1) * effType[key] else { hasUnknown = true } // Should not happen } }) if (hasVar) console.warn("CSSMathProduct.type(): Type includes var(), result indeterminate.") if (hasUnknown) console.warn("CSSMathProduct.type(): Operands included unknown types.") // Basic percentage simplification: L * % -> L etc. if (combined.percent === 1) { const otherDims = Object.keys(combined).filter(k => k !== 'percent' && combined[k] === 1) if (otherDims.length === 1 && Object.values(combined).filter(v => v !== 0).length === 2) { combined.percent = 0 // Remove percent if combined like L^1 * %^1 } } return combined } } // --- CSSMathNegate --- class CSSMathNegate extends CSSMathValue { constructor(value) { super() if (!(value instanceof CSSNumericValue || value instanceof CSSVariableReferenceValue)) throw new TypeError("CSSMathNegate needs CSSNumericValue or CSSVariableReferenceValue.") this._value = value this._operands = Object.freeze([value]) } get operator() { return "negate" } get value() { return this._value } toString() { return `calc(-1 * ${this._formatOperand(this._value, 'product')})` } // Use format helper type() { return this._value.type() } // Type delegates to inner value } // --- CSSMathInvert --- class CSSMathInvert extends CSSMathValue { constructor(value) { super() if (!(value instanceof CSSNumericValue || value instanceof CSSVariableReferenceValue)) throw new TypeError("CSSMathInvert needs CSSNumericValue or CSSVariableReferenceValue.") // Check zero early if ((value instanceof CSSUnitValue && value.value === 0) || (value instanceof CSSNumericValue && value._unitType === NUMBER_UNIT_TYPE && value._value === 0)) { throw new RangeError("Division by zero (inversion of 0).") } this._value = value this._operands = Object.freeze([value]) } get operator() { return "invert" } get value() { return this._value } toString() { return `calc(1 / ${this._formatOperand(this._value, 'product')})` } type() { const valType = this._value.type() const invType = {} for (const key in valType) { invType[key] = -valType[key] } return (this._value instanceof CSSVariableReferenceValue) ? {} : invType } // Handle inner var() } class CSSMathMin extends CSSMathValue { constructor(...operands) { super() if (operands.length === 0) throw new TypeError("CSSMathMin needs >= 1 argument.") this._operands = Object.freeze(operands.map((op, i) => { if (op instanceof CSSNumericValue || op instanceof CSSVariableReferenceValue) return op throw new TypeError(`CSSMathMin operand ${i + 1} invalid type ${op?.constructor?.name}`) })) } get operator() { return "min" } toString() { return `min(${this._operands.map(op => op.toString()).join(", ")})` } type() { return this._calculateMinMaxType() } _calculateMinMaxType() { let commonType = null let hasVar = false let hasUnknown = false for (const op of this._operands) { let opType if (op instanceof CSSVariableReferenceValue) { hasVar = true opType = {} } else if (op instanceof CSSNumericValue) { opType = op.type() } else { hasUnknown = true continue } const isOpVar = (op instanceof CSSVariableReferenceValue) if (Object.keys(opType).length === 0 && !isOpVar && !(op instanceof CSSNumericValue && op._unitType === NUMBER_UNIT_TYPE)) { hasUnknown = true continue } if (!commonType && !hasUnknown) { commonType = { ...opType } } else if (!hasUnknown && !isOpVar) { /* Check compatibility (length/percent mix allowed) */ const commonCat = Object.keys(commonType).find(k => commonType[k] !== 0 && k !== 'percent') const opCat = Object.keys(opType).find(k => opType[k] !== 0 && k !== 'percent') let compat = false if (!commonCat && !opCat) compat = true // num/perc + num/perc else if (commonCat === 'length' && !opCat) compat = true // len + num/perc else if (!commonCat && opCat === 'length') compat = true // num/perc + len else if (commonCat && commonCat === opCat) compat = true // unit + same unit if (compat) { if (opType.percent) commonType.percent = 1 /* Update mix */ } else { console.error(`${this.constructor.name} incompatible types: {${Object.keys(commonType).filter(k => commonType[k] !== 0)}} vs {${Object.keys(opType).filter(k => opType[k] !== 0)}}`) return {} } } } if (hasUnknown && !commonType) return {} if (hasVar) console.warn(`${this.constructor.name}.type(): Includes var(), result indeterminate.`) return commonType || {} } } class CSSMathMax extends CSSMathValue { constructor(...operands) { super() if (operands.length === 0) throw new TypeError("CSSMathMax needs >= 1 argument.") this._operands = Object.freeze(operands.map((op, i) => { if (op instanceof CSSNumericValue || op instanceof CSSVariableReferenceValue) return op throw new TypeError(`CSSMathMax operand ${i + 1} invalid type ${op?.constructor?.name}`) })) } get operator() { return "max" } toString() { return `max(${this._operands.map(op => op.toString()).join(", ")})` } type() { return CSSMathMin.prototype._calculateMinMaxType.call(this) } } // --- CSSImageValue (Stub) --- class CSSImageValue extends CSSStyleValue { constructor(t) { super() this._cssText = t } toString() { return this._cssText } } // --- CSSPositionValue (Stub) --- class CSSPositionValue extends CSSStyleValue { constructor(x, y) { super() this.x = x this.y = y /* simplified */ } toString() { return `${this.x} ${this.y}` } } // --- CSSTransformValue Stubs --- class CSSTransformComponent extends CSSStyleValue { constructor() { super() if (this.constructor === CSSTransformComponent) throw new TypeError("Abstract") this.is2D = true } } class CSSTranslate extends CSSTransformComponent { constructor(x, y, z = null) { super() this.x = x this.y = y this.z = z this.is2D = (z === null) } toString() { return this.is2D ? `translate(${this.x}, ${this.y})` : `translate3d(${this.x}, ${this.y}, ${this.z})` } } class CSSRotate extends CSSTransformComponent { constructor(angleOrX, y = null, z = null, angle = null) { super() this.is2D = (y === null && z === null && angle === null) if (this.is2D) { if (!(angleOrX instanceof CSSNumericValue) || angleOrX.type().angle !== 1) { // Allow unitless 0? Spec says <angle> if (!((angleOrX instanceof CSSUnitValue && angleOrX.value === 0) || (angleOrX instanceof CSSNumericValue && angleOrX._unitType === NUMBER_UNIT_TYPE && angleOrX._value === 0))) { throw new TypeError(`CSSRotate angle must be CSSNumericValue of type angle, got ${angleOrX?.toString()}`) } } this.angle = angleOrX this.x = this.y = this.z = CSS.number(0); // Store axis for consistency? No, keep null per spec. } else { // rotate3d(x, y, z, angle) const parseNum = (n, name) => { if (typeof n !== 'number') throw new TypeError(`CSSRotate ${name} must be a number.`) return CSS.number(n); // Store as CSSNumericValue internally } this.x = parseNum(angleOrX, 'x') this.y = parseNum(y, 'y') this.z = parseNum(z, 'z') if (!(angle instanceof CSSNumericValue) || angle.type().angle !== 1) { if (!((angle instanceof CSSUnitValue && angle.value === 0) || (angle instanceof CSSNumericValue && angle._unitType === NUMBER_UNIT_TYPE && angle._value === 0))) { throw new TypeError(`CSSRotate 3D angle must be CSSNumericValue of type angle, got ${angle?.toString()}`) } } this.angle = angle } } toString() { return this.is2D ? `rotate(${this.angle})` : `rotate3d(${this.x._value}, ${this.y._value}, ${this.z._value}, ${this.angle.toString()})` } } class CSSScale extends CSSTransformComponent { constructor(x, y, z = null) { super() const parseArg = (arg, name) => { let numVal if (arg instanceof CSSNumericValue) { if (Object.values(arg.type()).some(v => v !== 0)) { // Must be unitless throw new TypeError(`CSSScale ${name} must be a unitless number, got ${arg.toString()}`) } numVal = arg; // Already correct type } else if (typeof arg === 'number' && isFinite(arg)) { numVal = CSS.number(arg); // Convert number to CSSNumericValue } else { throw new TypeError(`CSSScale ${name} must be a finite number or unitless CSSNumericValue.`) } return numVal } this.x = parseArg(x, 'x') // If y is explicitly undefined or null, it defaults to x. If provided, parse it. this.y = (y === undefined || y === null) ? this.x : parseArg(y, 'y') // If z is explicitly null, it's a 2D scale. If provided, parse it. if (z !== null) { this.z = parseArg(z, 'z') this.is2D = false } else { this.z = CSS.number(1); // Default z to 1 for internal matrix math, but mark as 2D conceptually this.is2D = true } } toString() { return this.is2D ? (this.x == this.y ? `scale(${this.x})` : `scale(${this.x}, ${this.y})`) : `scale3d(${this.x}, ${this.y}, ${this.z})` } } class CSSTransformValue extends CSSStyleValue { constructor(t = []) { super() if (!Array.isArray(t) || !t.every(i => i instanceof CSSTransformComponent)) throw new TypeError("Invalid args") this.transforms = Object.freeze([...t]) } get length() { return this.transforms.length } item(i) { return this.transforms[i] } get is2D() { return this.transforms.every(i => i.is2D) } toString() { return this.transforms.map(i => i.toString()).join(" ") } [Symbol.iterator]() { return this.transforms[Symbol.iterator]() } entries() { return this.transforms.entries(); } keys() { return this.transforms.keys(); } values() { return this.transforms.values(); } forEach(callback, thisArg) { this.transforms.forEach(callback, thisArg); } } // --- CSSStyleValue Static