Source code

Revision control

Copy as Markdown

Other Tools

/* 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 SPECIALVALUES = new Set(["initial", "inherit", "unset"]);
const {
InspectorCSSParserWrapper,
} = require("resource://devtools/shared/css/lexer.js");
loader.lazyRequireGetter(
this,
"CSS_ANGLEUNIT",
"resource://devtools/shared/css/constants.js",
true
);
/**
* 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,
classifyAngle,
};
CssAngle.prototype = {
// Still keep trying to lazy load properties-db by lazily getting ANGLEUNIT
get ANGLEUNIT() {
return CSS_ANGLEUNIT;
},
_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() {
const token = new InspectorCSSParserWrapper(this.authored).nextToken();
if (!token) {
return false;
}
return (
token.tokenType === "Dimension" &&
token.unit.toLowerCase() in this.ANGLEUNIT
);
},
get specialValue() {
return SPECIALVALUES.has(this.lowerCased) ? this.authored : null;
},
get deg() {
const invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
const angleUnit = classifyAngle(this.authored);
if (angleUnit === this.ANGLEUNIT.deg) {
// The angle is valid and is in degree.
return this.authored;
}
let degValue;
if (angleUnit === this.ANGLEUNIT.rad) {
// The angle is valid and is in radian.
degValue = this.authoredAngleValue / (Math.PI / 180);
}
if (angleUnit === this.ANGLEUNIT.grad) {
// The angle is valid and is in gradian.
degValue = this.authoredAngleValue * 0.9;
}
if (angleUnit === this.ANGLEUNIT.turn) {
// The angle is valid and is in turn.
degValue = this.authoredAngleValue * 360;
}
let unitStr = this.ANGLEUNIT.deg;
if (this._angleUnitUppercase === true) {
unitStr = unitStr.toUpperCase();
}
return `${Math.round(degValue * 100) / 100}${unitStr}`;
},
get rad() {
const invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
const unit = classifyAngle(this.authored);
if (unit === this.ANGLEUNIT.rad) {
// The angle is valid and is in radian.
return this.authored;
}
let radValue;
if (unit === this.ANGLEUNIT.deg) {
// The angle is valid and is in degree.
radValue = this.authoredAngleValue * (Math.PI / 180);
}
if (unit === this.ANGLEUNIT.grad) {
// The angle is valid and is in gradian.
radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180);
}
if (unit === this.ANGLEUNIT.turn) {
// The angle is valid and is in turn.
radValue = this.authoredAngleValue * 360 * (Math.PI / 180);
}
let unitStr = this.ANGLEUNIT.rad;
if (this._angleUnitUppercase === true) {
unitStr = unitStr.toUpperCase();
}
return `${Math.round(radValue * 10000) / 10000}${unitStr}`;
},
get grad() {
const invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
const unit = classifyAngle(this.authored);
if (unit === this.ANGLEUNIT.grad) {
// The angle is valid and is in gradian
return this.authored;
}
let gradValue;
if (unit === this.ANGLEUNIT.deg) {
// The angle is valid and is in degree
gradValue = this.authoredAngleValue / 0.9;
}
if (unit === this.ANGLEUNIT.rad) {
// The angle is valid and is in radian
gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180);
}
if (unit === this.ANGLEUNIT.turn) {
// The angle is valid and is in turn
gradValue = this.authoredAngleValue * 400;
}
let unitStr = this.ANGLEUNIT.grad;
if (this._angleUnitUppercase === true) {
unitStr = unitStr.toUpperCase();
}
return `${Math.round(gradValue * 100) / 100}${unitStr}`;
},
get turn() {
const invalidOrSpecialValue = this._getInvalidOrSpecialValue();
if (invalidOrSpecialValue !== false) {
return invalidOrSpecialValue;
}
const unit = classifyAngle(this.authored);
if (unit === this.ANGLEUNIT.turn) {
// The angle is valid and is in turn
return this.authored;
}
let turnValue;
if (unit === this.ANGLEUNIT.deg) {
// The angle is valid and is in degree
turnValue = this.authoredAngleValue / 360;
}
if (unit === this.ANGLEUNIT.rad) {
// The angle is valid and is in radian
turnValue = this.authoredAngleValue / (Math.PI / 180) / 360;
}
if (unit === this.ANGLEUNIT.grad) {
// The angle is valid and is in gradian
turnValue = this.authoredAngleValue / 400;
}
let unitStr = this.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() {
if (this.specialValue) {
return this.specialValue;
}
if (!this.valid) {
return "";
}
return false;
},
/**
* Change angle
*
* @param {String} angle
* Any valid angle value + unit string
*/
newAngle(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;
const reg = new RegExp(`(${Object.keys(this.ANGLEUNIT).join("|")})$`, "i");
const unitStartIdx = angle.search(reg);
this.authoredAngleValue = angle.substring(0, unitStartIdx);
this.authoredAngleUnit = angle.substring(unitStartIdx, angle.length);
return this;
},
nextAngleUnit() {
// 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(this.ANGLEUNIT);
const putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit));
formats = formats.concat(putOnEnd);
const currentDisplayedValue = this[formats[0]];
for (const format of formats) {
if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) {
this.angleUnit = this.ANGLEUNIT[format];
break;
}
}
return this.toString();
},
/**
* Return a string representing a angle
*/
toString() {
let angle;
switch (this.angleUnit) {
case this.ANGLEUNIT.deg:
angle = this.deg;
break;
case this.ANGLEUNIT.rad:
angle = this.rad;
break;
case this.ANGLEUNIT.grad:
angle = this.grad;
break;
case this.ANGLEUNIT.turn:
angle = this.turn;
break;
default:
angle = this.deg;
}
if (this._angleUnitUppercase && this.angleUnit != this.ANGLEUNIT.authored) {
angle = angle.toUpperCase();
}
return angle;
},
/**
* This method allows comparison of CssAngle objects using ===.
*/
valueOf() {
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 CSS_ANGLEUNIT.deg;
}
if (value.endsWith("grad")) {
return CSS_ANGLEUNIT.grad;
}
if (value.endsWith("rad")) {
return CSS_ANGLEUNIT.rad;
}
if (value.endsWith("turn")) {
return CSS_ANGLEUNIT.turn;
}
return CSS_ANGLEUNIT.deg;
}