/* This Source Code Form is subject to the terms of the Mozilla Public * License, v. 2.0. If a copy of the MPL was not distributed with this * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ "use strict"; const {CSS_ANGLEUNIT} = require("devtools/shared/css/properties-db"); const SPECIALVALUES = new Set([ "initial", "inherit", "unset" ]); const {getCSSLexer} = require("devtools/shared/css/lexer"); /** * This module is used to convert between various angle units. * * Usage: * let {angleUtils} = require("devtools/client/shared/css-angle"); * let angle = new angleUtils.CssAngle("180deg"); * * angle.authored === "180deg" * angle.valid === true * angle.rad === "3,14rad" * angle.grad === "200grad" * angle.turn === "0.5turn" * * angle.toString() === "180deg"; // Outputs the angle value and its unit * // Angle objects can be reused * angle.newAngle("-1TURN") === "-1TURN"; // true */ function CssAngle(angleValue) { this.newAngle(angleValue); } module.exports.angleUtils = { CssAngle: CssAngle, classifyAngle: classifyAngle }; CssAngle.ANGLEUNIT = CSS_ANGLEUNIT; CssAngle.prototype = { _angleUnit: null, _angleUnitUppercase: false, // The value as-authored. authored: null, // A lower-cased copy of |authored|. lowerCased: null, get angleUnit() { if (this._angleUnit === null) { this._angleUnit = classifyAngle(this.authored); } return this._angleUnit; }, set angleUnit(unit) { this._angleUnit = unit; }, get valid() { let token = getCSSLexer(this.authored).nextToken(); if (!token) { return false; } return (token.tokenType === "dimension" && token.text.toLowerCase() in CssAngle.ANGLEUNIT); }, get specialValue() { return SPECIALVALUES.has(this.lowerCased) ? this.authored : null; }, get deg() { let invalidOrSpecialValue = this._getInvalidOrSpecialValue(); if (invalidOrSpecialValue !== false) { return invalidOrSpecialValue; } let angleUnit = classifyAngle(this.authored); if (angleUnit === CssAngle.ANGLEUNIT.deg) { // The angle is valid and is in degree. return this.authored; } let degValue; if (angleUnit === CssAngle.ANGLEUNIT.rad) { // The angle is valid and is in radian. degValue = this.authoredAngleValue / (Math.PI / 180); } if (angleUnit === CssAngle.ANGLEUNIT.grad) { // The angle is valid and is in gradian. degValue = this.authoredAngleValue * 0.9; } if (angleUnit === CssAngle.ANGLEUNIT.turn) { // The angle is valid and is in turn. degValue = this.authoredAngleValue * 360; } let unitStr = CssAngle.ANGLEUNIT.deg; if (this._angleUnitUppercase === true) { unitStr = unitStr.toUpperCase(); } return `${Math.round(degValue * 100) / 100}${unitStr}`; }, get rad() { let invalidOrSpecialValue = this._getInvalidOrSpecialValue(); if (invalidOrSpecialValue !== false) { return invalidOrSpecialValue; } let unit = classifyAngle(this.authored); if (unit === CssAngle.ANGLEUNIT.rad) { // The angle is valid and is in radian. return this.authored; } let radValue; if (unit === CssAngle.ANGLEUNIT.deg) { // The angle is valid and is in degree. radValue = this.authoredAngleValue * (Math.PI / 180); } if (unit === CssAngle.ANGLEUNIT.grad) { // The angle is valid and is in gradian. radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180); } if (unit === CssAngle.ANGLEUNIT.turn) { // The angle is valid and is in turn. radValue = this.authoredAngleValue * 360 * (Math.PI / 180); } let unitStr = CssAngle.ANGLEUNIT.rad; if (this._angleUnitUppercase === true) { unitStr = unitStr.toUpperCase(); } return `${Math.round(radValue * 10000) / 10000}${unitStr}`; }, get grad() { let invalidOrSpecialValue = this._getInvalidOrSpecialValue(); if (invalidOrSpecialValue !== false) { return invalidOrSpecialValue; } let unit = classifyAngle(this.authored); if (unit === CssAngle.ANGLEUNIT.grad) { // The angle is valid and is in gradian return this.authored; } let gradValue; if (unit === CssAngle.ANGLEUNIT.deg) { // The angle is valid and is in degree gradValue = this.authoredAngleValue / 0.9; } if (unit === CssAngle.ANGLEUNIT.rad) { // The angle is valid and is in radian gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180); } if (unit === CssAngle.ANGLEUNIT.turn) { // The angle is valid and is in turn gradValue = this.authoredAngleValue * 400; } let unitStr = CssAngle.ANGLEUNIT.grad; if (this._angleUnitUppercase === true) { unitStr = unitStr.toUpperCase(); } return `${Math.round(gradValue * 100) / 100}${unitStr}`; }, get turn() { let invalidOrSpecialValue = this._getInvalidOrSpecialValue(); if (invalidOrSpecialValue !== false) { return invalidOrSpecialValue; } let unit = classifyAngle(this.authored); if (unit === CssAngle.ANGLEUNIT.turn) { // The angle is valid and is in turn return this.authored; } let turnValue; if (unit === CssAngle.ANGLEUNIT.deg) { // The angle is valid and is in degree turnValue = this.authoredAngleValue / 360; } if (unit === CssAngle.ANGLEUNIT.rad) { // The angle is valid and is in radian turnValue = (this.authoredAngleValue / (Math.PI / 180)) / 360; } if (unit === CssAngle.ANGLEUNIT.grad) { // The angle is valid and is in gradian turnValue = this.authoredAngleValue / 400; } let unitStr = CssAngle.ANGLEUNIT.turn; if (this._angleUnitUppercase === true) { unitStr = unitStr.toUpperCase(); } return `${Math.round(turnValue * 100) / 100}${unitStr}`; }, /** * Check whether the angle value is in the special list e.g. * inherit or invalid. * * @return {String|Boolean} * - If the current angle is a special value e.g. "inherit" then * return the angle. * - If the angle is invalid return an empty string. * - If the angle is a regular angle e.g. 90deg so we return false * to indicate that the angle is neither invalid nor special. */ _getInvalidOrSpecialValue: function () { if (this.specialValue) { return this.specialValue; } if (!this.valid) { return ""; } return false; }, /** * Change angle * * @param {String} angle * Any valid angle value + unit string */ newAngle: function (angle) { // Store a lower-cased version of the angle to help with format // testing. The original text is kept as well so it can be // returned when needed. this.lowerCased = angle.toLowerCase(); this._angleUnitUppercase = (angle === angle.toUpperCase()); this.authored = angle; let reg = new RegExp( `(${Object.keys(CssAngle.ANGLEUNIT).join("|")})$`, "i"); let unitStartIdx = angle.search(reg); this.authoredAngleValue = angle.substring(0, unitStartIdx); this.authoredAngleUnit = angle.substring(unitStartIdx, angle.length); return this; }, nextAngleUnit: function () { // Get a reordered array from the formats object // to have the current format at the front so we can cycle through. let formats = Object.keys(CssAngle.ANGLEUNIT); let putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit)); formats = formats.concat(putOnEnd); let currentDisplayedValue = this[formats[0]]; for (let format of formats) { if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) { this.angleUnit = CssAngle.ANGLEUNIT[format]; break; } } return this.toString(); }, /** * Return a string representing a angle */ toString: function () { let angle; switch (this.angleUnit) { case CssAngle.ANGLEUNIT.deg: angle = this.deg; break; case CssAngle.ANGLEUNIT.rad: angle = this.rad; break; case CssAngle.ANGLEUNIT.grad: angle = this.grad; break; case CssAngle.ANGLEUNIT.turn: angle = this.turn; break; default: angle = this.deg; } if (this._angleUnitUppercase && this.angleUnit != CssAngle.ANGLEUNIT.authored) { angle = angle.toUpperCase(); } return angle; }, /** * This method allows comparison of CssAngle objects using ===. */ valueOf: function () { return this.deg; }, }; /** * Given a color, classify its type as one of the possible angle * units, as known by |CssAngle.angleUnit|. * * @param {String} value * The angle, in any form accepted by CSS. * @return {String} * The angle classification, one of "deg", "rad", "grad", or "turn". */ function classifyAngle(value) { value = value.toLowerCase(); if (value.endsWith("deg")) { return CssAngle.ANGLEUNIT.deg; } if (value.endsWith("grad")) { return CssAngle.ANGLEUNIT.grad; } if (value.endsWith("rad")) { return CssAngle.ANGLEUNIT.rad; } if (value.endsWith("turn")) { return CssAngle.ANGLEUNIT.turn; } return CssAngle.ANGLEUNIT.deg; }