css-angle.js (9303B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const SPECIALVALUES = new Set(["initial", "inherit", "unset"]); 8 9 const { 10 InspectorCSSParserWrapper, 11 } = require("resource://devtools/shared/css/lexer.js"); 12 13 loader.lazyRequireGetter( 14 this, 15 "CSS_ANGLEUNIT", 16 "resource://devtools/shared/css/constants.js", 17 true 18 ); 19 20 /** 21 * This module is used to convert between various angle units. 22 * 23 * Usage: 24 * let {angleUtils} = require("devtools/client/shared/css-angle"); 25 * let angle = new angleUtils.CssAngle("180deg"); 26 * 27 * angle.authored === "180deg" 28 * angle.valid === true 29 * angle.rad === "3,14rad" 30 * angle.grad === "200grad" 31 * angle.turn === "0.5turn" 32 * 33 * angle.toString() === "180deg"; // Outputs the angle value and its unit 34 * // Angle objects can be reused 35 * angle.newAngle("-1TURN") === "-1TURN"; // true 36 */ 37 38 class CssAngle { 39 constructor(angleValue) { 40 this.newAngle(angleValue); 41 } 42 43 // Still keep trying to lazy load properties-db by lazily getting ANGLEUNIT 44 get ANGLEUNIT() { 45 return CSS_ANGLEUNIT; 46 } 47 48 _angleUnit = null; 49 _angleUnitUppercase = false; 50 51 // The value as-authored. 52 authored = null; 53 // A lower-cased copy of |authored|. 54 lowerCased = null; 55 56 get angleUnit() { 57 if (this._angleUnit === null) { 58 this._angleUnit = classifyAngle(this.authored); 59 } 60 return this._angleUnit; 61 } 62 63 set angleUnit(unit) { 64 this._angleUnit = unit; 65 } 66 67 get valid() { 68 const token = new InspectorCSSParserWrapper(this.authored).nextToken(); 69 if (!token) { 70 return false; 71 } 72 73 return ( 74 token.tokenType === "Dimension" && 75 token.unit.toLowerCase() in this.ANGLEUNIT 76 ); 77 } 78 79 get specialValue() { 80 return SPECIALVALUES.has(this.lowerCased) ? this.authored : null; 81 } 82 83 get deg() { 84 const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); 85 if (invalidOrSpecialValue !== false) { 86 return invalidOrSpecialValue; 87 } 88 89 const angleUnit = classifyAngle(this.authored); 90 if (angleUnit === this.ANGLEUNIT.deg) { 91 // The angle is valid and is in degree. 92 return this.authored; 93 } 94 95 let degValue; 96 if (angleUnit === this.ANGLEUNIT.rad) { 97 // The angle is valid and is in radian. 98 degValue = this.authoredAngleValue / (Math.PI / 180); 99 } 100 101 if (angleUnit === this.ANGLEUNIT.grad) { 102 // The angle is valid and is in gradian. 103 degValue = this.authoredAngleValue * 0.9; 104 } 105 106 if (angleUnit === this.ANGLEUNIT.turn) { 107 // The angle is valid and is in turn. 108 degValue = this.authoredAngleValue * 360; 109 } 110 111 let unitStr = this.ANGLEUNIT.deg; 112 if (this._angleUnitUppercase === true) { 113 unitStr = unitStr.toUpperCase(); 114 } 115 return `${Math.round(degValue * 100) / 100}${unitStr}`; 116 } 117 118 get rad() { 119 const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); 120 if (invalidOrSpecialValue !== false) { 121 return invalidOrSpecialValue; 122 } 123 124 const unit = classifyAngle(this.authored); 125 if (unit === this.ANGLEUNIT.rad) { 126 // The angle is valid and is in radian. 127 return this.authored; 128 } 129 130 let radValue; 131 if (unit === this.ANGLEUNIT.deg) { 132 // The angle is valid and is in degree. 133 radValue = this.authoredAngleValue * (Math.PI / 180); 134 } 135 136 if (unit === this.ANGLEUNIT.grad) { 137 // The angle is valid and is in gradian. 138 radValue = this.authoredAngleValue * 0.9 * (Math.PI / 180); 139 } 140 141 if (unit === this.ANGLEUNIT.turn) { 142 // The angle is valid and is in turn. 143 radValue = this.authoredAngleValue * 360 * (Math.PI / 180); 144 } 145 146 let unitStr = this.ANGLEUNIT.rad; 147 if (this._angleUnitUppercase === true) { 148 unitStr = unitStr.toUpperCase(); 149 } 150 return `${Math.round(radValue * 10000) / 10000}${unitStr}`; 151 } 152 153 get grad() { 154 const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); 155 if (invalidOrSpecialValue !== false) { 156 return invalidOrSpecialValue; 157 } 158 159 const unit = classifyAngle(this.authored); 160 if (unit === this.ANGLEUNIT.grad) { 161 // The angle is valid and is in gradian 162 return this.authored; 163 } 164 165 let gradValue; 166 if (unit === this.ANGLEUNIT.deg) { 167 // The angle is valid and is in degree 168 gradValue = this.authoredAngleValue / 0.9; 169 } 170 171 if (unit === this.ANGLEUNIT.rad) { 172 // The angle is valid and is in radian 173 gradValue = this.authoredAngleValue / 0.9 / (Math.PI / 180); 174 } 175 176 if (unit === this.ANGLEUNIT.turn) { 177 // The angle is valid and is in turn 178 gradValue = this.authoredAngleValue * 400; 179 } 180 181 let unitStr = this.ANGLEUNIT.grad; 182 if (this._angleUnitUppercase === true) { 183 unitStr = unitStr.toUpperCase(); 184 } 185 return `${Math.round(gradValue * 100) / 100}${unitStr}`; 186 } 187 188 get turn() { 189 const invalidOrSpecialValue = this._getInvalidOrSpecialValue(); 190 if (invalidOrSpecialValue !== false) { 191 return invalidOrSpecialValue; 192 } 193 194 const unit = classifyAngle(this.authored); 195 if (unit === this.ANGLEUNIT.turn) { 196 // The angle is valid and is in turn 197 return this.authored; 198 } 199 200 let turnValue; 201 if (unit === this.ANGLEUNIT.deg) { 202 // The angle is valid and is in degree 203 turnValue = this.authoredAngleValue / 360; 204 } 205 206 if (unit === this.ANGLEUNIT.rad) { 207 // The angle is valid and is in radian 208 turnValue = this.authoredAngleValue / (Math.PI / 180) / 360; 209 } 210 211 if (unit === this.ANGLEUNIT.grad) { 212 // The angle is valid and is in gradian 213 turnValue = this.authoredAngleValue / 400; 214 } 215 216 let unitStr = this.ANGLEUNIT.turn; 217 if (this._angleUnitUppercase === true) { 218 unitStr = unitStr.toUpperCase(); 219 } 220 return `${Math.round(turnValue * 100) / 100}${unitStr}`; 221 } 222 223 /** 224 * Check whether the angle value is in the special list e.g. 225 * inherit or invalid. 226 * 227 * @return {string | boolean} 228 * - If the current angle is a special value e.g. "inherit" then 229 * return the angle. 230 * - If the angle is invalid return an empty string. 231 * - If the angle is a regular angle e.g. 90deg so we return false 232 * to indicate that the angle is neither invalid nor special. 233 */ 234 _getInvalidOrSpecialValue() { 235 if (this.specialValue) { 236 return this.specialValue; 237 } 238 if (!this.valid) { 239 return ""; 240 } 241 return false; 242 } 243 244 /** 245 * Change angle 246 * 247 * @param {string} angle 248 * Any valid angle value + unit string 249 */ 250 newAngle(angle) { 251 // Store a lower-cased version of the angle to help with format 252 // testing. The original text is kept as well so it can be 253 // returned when needed. 254 this.lowerCased = angle.toLowerCase(); 255 this._angleUnitUppercase = angle === angle.toUpperCase(); 256 this.authored = angle; 257 258 const reg = new RegExp(`(${Object.keys(this.ANGLEUNIT).join("|")})$`, "i"); 259 const unitStartIdx = angle.search(reg); 260 this.authoredAngleValue = angle.substring(0, unitStartIdx); 261 this.authoredAngleUnit = angle.substring(unitStartIdx, angle.length); 262 263 return this; 264 } 265 266 nextAngleUnit() { 267 // Get a reordered array from the formats object 268 // to have the current format at the front so we can cycle through. 269 let formats = Object.keys(this.ANGLEUNIT); 270 const putOnEnd = formats.splice(0, formats.indexOf(this.angleUnit)); 271 formats = formats.concat(putOnEnd); 272 const currentDisplayedValue = this[formats[0]]; 273 274 for (const format of formats) { 275 if (this[format].toLowerCase() !== currentDisplayedValue.toLowerCase()) { 276 this.angleUnit = this.ANGLEUNIT[format]; 277 break; 278 } 279 } 280 return this.toString(); 281 } 282 283 /** 284 * Return a string representing a angle 285 */ 286 toString() { 287 let angle; 288 289 switch (this.angleUnit) { 290 case this.ANGLEUNIT.deg: 291 angle = this.deg; 292 break; 293 case this.ANGLEUNIT.rad: 294 angle = this.rad; 295 break; 296 case this.ANGLEUNIT.grad: 297 angle = this.grad; 298 break; 299 case this.ANGLEUNIT.turn: 300 angle = this.turn; 301 break; 302 default: 303 angle = this.deg; 304 } 305 306 if (this._angleUnitUppercase && this.angleUnit != this.ANGLEUNIT.authored) { 307 angle = angle.toUpperCase(); 308 } 309 return angle; 310 } 311 312 /** 313 * This method allows comparison of CssAngle objects using ===. 314 */ 315 valueOf() { 316 return this.deg; 317 } 318 } 319 320 /** 321 * Given a color, classify its type as one of the possible angle 322 * units, as known by |CssAngle.angleUnit|. 323 * 324 * @param {string} value 325 * The angle, in any form accepted by CSS. 326 * @return {string} 327 * The angle classification, one of "deg", "rad", "grad", or "turn". 328 */ 329 function classifyAngle(value) { 330 value = value.toLowerCase(); 331 if (value.endsWith("deg")) { 332 return CSS_ANGLEUNIT.deg; 333 } 334 335 if (value.endsWith("grad")) { 336 return CSS_ANGLEUNIT.grad; 337 } 338 339 if (value.endsWith("rad")) { 340 return CSS_ANGLEUNIT.rad; 341 } 342 if (value.endsWith("turn")) { 343 return CSS_ANGLEUNIT.turn; 344 } 345 346 return CSS_ANGLEUNIT.deg; 347 } 348 349 module.exports.angleUtils = { 350 CssAngle, 351 classifyAngle, 352 };