--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -332,25 +332,28 @@ TextPropertyEditor.prototype = {
} else {
this.element.removeAttribute("dirty");
}
const sharedSwatchClass = "ruleview-swatch ";
const colorSwatchClass = "ruleview-colorswatch";
const bezierSwatchClass = "ruleview-bezierswatch";
const filterSwatchClass = "ruleview-filterswatch";
+ const angleSwatchClass = "ruleview-angleswatch";
let outputParser = this.ruleView._outputParser;
let parserOptions = {
colorSwatchClass: sharedSwatchClass + colorSwatchClass,
colorClass: "ruleview-color",
bezierSwatchClass: sharedSwatchClass + bezierSwatchClass,
bezierClass: "ruleview-bezier",
filterSwatchClass: sharedSwatchClass + filterSwatchClass,
filterClass: "ruleview-filter",
+ angleSwatchClass: sharedSwatchClass + angleSwatchClass,
+ angleClass: "ruleview-angle",
defaultColorType: !propDirty,
urlClass: "theme-link",
baseURI: this.sheetURI
};
let frag = outputParser.parseCssProperty(name, val, parserOptions);
this.valueSpan.innerHTML = "";
this.valueSpan.appendChild(frag);
--- a/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js
+++ b/devtools/client/inspector/shared/test/browser_styleinspector_output-parser.js
@@ -9,16 +9,17 @@
// This is more of a unit test than a mochitest-browser test, but can't be
// tested with an xpcshell test as the output-parser requires the DOM to work.
var {OutputParser} = require("devtools/client/shared/output-parser");
const COLOR_CLASS = "color-class";
const URL_CLASS = "url-class";
const CUBIC_BEZIER_CLASS = "bezier-class";
+const ANGLE_CLASS = "angle-class";
const TEST_DATA = [
{
name: "width",
value: "100%",
test: fragment => {
is(countAll(fragment), 0);
is(fragment.textContent, "100%");
@@ -155,21 +156,24 @@ const TEST_DATA = [
is(allSwatches[3].textContent, "#F06");
is(allSwatches[4].textContent, "red");
}
},
{
name: "background",
value: "-moz-radial-gradient(center 45deg, circle closest-side, orange 0%, red 100%)",
test: fragment => {
- is(countAll(fragment), 4);
- let allSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
- is(allSwatches.length, 2);
- is(allSwatches[0].textContent, "orange");
- is(allSwatches[1].textContent, "red");
+ is(countAll(fragment), 6);
+ let colorSwatches = fragment.querySelectorAll("." + COLOR_CLASS);
+ is(colorSwatches.length, 2);
+ is(colorSwatches[0].textContent, "orange");
+ is(colorSwatches[1].textContent, "red");
+ let angleSwatches = fragment.querySelectorAll("." + ANGLE_CLASS);
+ is(angleSwatches.length, 1);
+ is(angleSwatches[0].textContent, "45deg");
}
},
{
name: "background",
value: "white url(http://test.com/wow_such_image.png) no-repeat top left",
test: fragment => {
is(countAll(fragment), 3);
is(countUrls(fragment), 1);
@@ -291,16 +295,17 @@ add_task(function*() {
for (let i = 0; i < TEST_DATA.length; i++) {
let data = TEST_DATA[i];
info("Output-parser test data " + i + ". {" + data.name + " : " +
data.value + ";}");
data.test(parser.parseCssProperty(data.name, data.value, {
colorClass: COLOR_CLASS,
urlClass: URL_CLASS,
bezierClass: CUBIC_BEZIER_CLASS,
+ angleClass: ANGLE_CLASS,
defaultColorType: false
}));
}
});
function countAll(fragment) {
return fragment.querySelectorAll("*").length;
}
--- a/devtools/client/jar.mn
+++ b/devtools/client/jar.mn
@@ -149,16 +149,17 @@ devtools.jar:
skin/dark-theme.css (themes/dark-theme.css)
skin/light-theme.css (themes/light-theme.css)
skin/firebug-theme.css (themes/firebug-theme.css)
skin/toolbars.css (themes/toolbars.css)
skin/variables.css (themes/variables.css)
skin/images/add.svg (themes/images/add.svg)
skin/images/filters.svg (themes/images/filters.svg)
skin/images/filter-swatch.svg (themes/images/filter-swatch.svg)
+ skin/images/angle-swatch.svg (themes/images/angle-swatch.svg)
skin/images/pseudo-class.svg (themes/images/pseudo-class.svg)
skin/images/controls.png (themes/images/controls.png)
skin/images/controls@2x.png (themes/images/controls@2x.png)
skin/images/animation-fast-track.svg (themes/images/animation-fast-track.svg)
skin/images/performance-icons.svg (themes/images/performance-icons.svg)
skin/widgets.css (themes/widgets.css)
skin/images/power.svg (themes/images/power.svg)
skin/images/filetypes/dir-close.svg (themes/images/filetypes/dir-close.svg)
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -1,15 +1,16 @@
/* 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 {Cc, Ci, Cu} = require("chrome");
+const {angleUtils} = require("devtools/shared/css-angle");
const {colorUtils} = require("devtools/shared/css-color");
const Services = require("Services");
const HTML_NS = "http://www.w3.org/1999/xhtml";
const BEZIER_KEYWORDS = ["linear", "ease-in-out", "ease-in", "ease-out",
"ease"];
@@ -19,16 +20,31 @@ const COLOR_TAKING_FUNCTIONS = ["linear-
"repeating-linear-gradient",
"-moz-repeating-linear-gradient",
"radial-gradient",
"-moz-radial-gradient",
"repeating-radial-gradient",
"-moz-repeating-radial-gradient",
"drop-shadow"];
+// Functions that accept an angle argument.
+const ANGLE_TAKING_FUNCTIONS = ["linear-gradient",
+ "-moz-linear-gradient",
+ "repeating-linear-gradient",
+ "-moz-repeating-linear-gradient",
+ "rotate",
+ "rotateX",
+ "rotateY",
+ "rotateZ",
+ "rotate3d",
+ "skew",
+ "skewX",
+ "skewY",
+ "hue-rotate"];
+
loader.lazyGetter(this, "DOMUtils", function() {
return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
});
/**
* This module is used to process text for output by developer tools. This means
* linking JS files with the debugger, CSS files with the style editor, JS
* functions with the debugger, placing color swatches next to colors and
@@ -43,17 +59,19 @@ loader.lazyGetter(this, "DOMUtils", func
* let parser = new OutputParser(document);
*
* parser.parseCssProperty("color", "red"); // Returns document fragment.
*/
function OutputParser(document) {
this.parsed = [];
this.doc = document;
this.colorSwatches = new WeakMap();
- this._onSwatchMouseDown = this._onSwatchMouseDown.bind(this);
+ this.angleSwatches = new WeakMap();
+ this._onColorSwatchMouseDown = this._onColorSwatchMouseDown.bind(this);
+ this._onAngleSwatchMouseDown = this._onAngleSwatchMouseDown.bind(this);
}
exports.OutputParser = OutputParser;
OutputParser.prototype = {
/**
* Parse a CSS property value given a property name.
*
@@ -62,17 +80,17 @@ OutputParser.prototype = {
* @param {String} value
* CSS Property value
* @param {Object} [options]
* Options object. For valid options and default values see
* _mergeOptions().
* @return {DocumentFragment}
* A document fragment containing color swatches etc.
*/
- parseCssProperty: function(name, value, options={}) {
+ parseCssProperty: function(name, value, options = {}) {
options = this._mergeOptions(options);
options.expectCubicBezier =
safeCssPropertySupportsType(name, DOMUtils.TYPE_TIMING_FUNCTION);
options.expectFilter = name === "filter";
options.supportsColor =
safeCssPropertySupportsType(name, DOMUtils.TYPE_COLOR) ||
safeCssPropertySupportsType(name, DOMUtils.TYPE_GRADIENT);
@@ -135,50 +153,56 @@ OutputParser.prototype = {
* @param {String} text
* Text to parse.
* @param {Object} [options]
* Options object. For valid options and default values see
* _mergeOptions().
* @return {DocumentFragment}
* A document fragment.
*/
- _parse: function(text, options={}) {
+ _parse: function(text, options = {}) {
text = text.trim();
this.parsed.length = 0;
let tokenStream = DOMUtils.getCSSLexer(text);
let parenDepth = 0;
let outerMostFunctionTakesColor = false;
let colorOK = function() {
return options.supportsColor ||
(options.expectFilter && parenDepth === 1 &&
outerMostFunctionTakesColor);
};
+ let angleOK = function(angle) {
+ return /^-?\d+\.?\d*(deg|rad|grad|turn)$/gi.test(angle);
+ };
+
while (true) {
let token = tokenStream.nextToken();
if (!token) {
break;
}
if (token.tokenType === "comment") {
continue;
}
switch (token.tokenType) {
case "function": {
- if (COLOR_TAKING_FUNCTIONS.indexOf(token.text) >= 0) {
- // The function can accept a color argument, and we know
- // it isn't special in some other way. So, we let it
- // through to the ordinary parsing loop so that colors
+ if (COLOR_TAKING_FUNCTIONS.includes(token.text) ||
+ ANGLE_TAKING_FUNCTIONS.includes(token.text)) {
+ // The function can accept a color or an angle argument, and we know
+ // it isn't special in some other way. So, we let it
+ // through to the ordinary parsing loop so that the value
// can be handled in a single place.
this._appendTextNode(text.substring(token.startOffset,
token.endOffset));
if (parenDepth === 0) {
- outerMostFunctionTakesColor = true;
+ outerMostFunctionTakesColor = COLOR_TAKING_FUNCTIONS.includes(
+ token.text);
}
++parenDepth;
} else {
let functionText = this._collectFunctionText(token, text,
tokenStream);
if (options.expectCubicBezier && token.text === "cubic-bezier") {
this._appendCubicBezier(functionText, options);
@@ -192,49 +216,61 @@ OutputParser.prototype = {
}
case "ident":
if (options.expectCubicBezier &&
BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
this._appendCubicBezier(token.text, options);
} else if (colorOK() && DOMUtils.isValidCSSColor(token.text)) {
this._appendColor(token.text, options);
+ } else if (angleOK(token.text)) {
+ this._appendAngle(token.text, options);
} else {
this._appendTextNode(text.substring(token.startOffset,
token.endOffset));
}
break;
case "id":
case "hash": {
let original = text.substring(token.startOffset, token.endOffset);
if (colorOK() && DOMUtils.isValidCSSColor(original)) {
this._appendColor(original, options);
} else {
this._appendTextNode(original);
}
break;
}
-
+ case "dimension":
+ let value = text.substring(token.startOffset, token.endOffset);
+ if (angleOK(value)) {
+ this._appendAngle(value, options);
+ } else {
+ this._appendTextNode(value);
+ }
+ break;
case "url":
case "bad_url":
this._appendURL(text.substring(token.startOffset, token.endOffset),
token.text, options);
break;
case "symbol":
if (token.text === "(") {
++parenDepth;
- } else if (token.token === ")") {
+ } else if (token.text === ")") {
--parenDepth;
+ if (parenDepth === 0) {
+ outerMostFunctionTakesColor = false;
+ }
}
// falls through
default:
- this._appendTextNode(text.substring(token.startOffset,
- token.endOffset));
+ this._appendTextNode(
+ text.substring(token.startOffset, token.endOffset));
break;
}
}
let result = this._toDOM();
if (options.expectFilter && !options.filterSwatch) {
result = this._wrapFilter(text, options, result);
@@ -249,17 +285,17 @@ OutputParser.prototype = {
* @param {String} bezier
* The cubic-bezier timing function
* @param {Object} options
* Options object. For valid options and default values see
* _mergeOptions()
*/
_appendCubicBezier: function(bezier, options) {
let container = this._createNode("span", {
- "data-bezier": bezier
+ "data-bezier": bezier
});
if (options.bezierSwatchClass) {
let swatch = this._createNode("span", {
class: options.bezierSwatchClass
});
container.appendChild(swatch);
}
@@ -268,16 +304,58 @@ OutputParser.prototype = {
class: options.bezierClass
}, bezier);
container.appendChild(value);
this.parsed.push(container);
},
/**
+ * Append a angle value to the output
+ *
+ * @param {String} angle
+ * angle to append
+ * @param {Object} options
+ * Options object. For valid options and default values see
+ * _mergeOptions()
+ */
+ _appendAngle: function(angle, options) {
+ let angleObj = new angleUtils.CssAngle(angle);
+ let container = this._createNode("span", {
+ "data-angle": angle
+ });
+
+ if (options.angleSwatchClass) {
+ let swatch = this._createNode("span", {
+ class: options.angleSwatchClass
+ });
+ this.angleSwatches.set(swatch, angleObj);
+ swatch.addEventListener("mousedown", this._onAngleSwatchMouseDown, false);
+
+ // Add click listener to stop event propagation when shift key is pressed
+ // in order to prevent the value input to be focused.
+ // Bug 711942 will add a tooltip to edit angle values and we should
+ // be able to move this listener to Tooltip.js when it'll be implemented.
+ swatch.addEventListener("click", function(event) {
+ if (event.shiftKey) {
+ event.stopPropagation();
+ }
+ }, false);
+ container.appendChild(swatch);
+ }
+
+ let value = this._createNode("span", {
+ class: options.angleClass
+ }, angle);
+
+ container.appendChild(value);
+ this.parsed.push(container);
+ },
+
+ /**
* Check if a CSS property supports a specific value.
*
* @param {String} name
* CSS Property name to check
* @param {String} value
* CSS Property value to check
*/
_cssPropertySupportsValue: function(name, value) {
@@ -312,17 +390,17 @@ OutputParser.prototype = {
});
if (options.colorSwatchClass) {
let swatch = this._createNode("span", {
class: options.colorSwatchClass,
style: "background-color:" + color
});
this.colorSwatches.set(swatch, colorObj);
- swatch.addEventListener("mousedown", this._onSwatchMouseDown, false);
+ swatch.addEventListener("mousedown", this._onColorSwatchMouseDown, false);
container.appendChild(swatch);
}
if (options.defaultColorType) {
color = colorObj.toString();
container.dataset.colorĀ = color;
}
@@ -366,31 +444,46 @@ OutputParser.prototype = {
class: options.filterClass
});
value.appendChild(nodes);
container.appendChild(value);
return container;
},
- _onSwatchMouseDown: function(event) {
+ _onColorSwatchMouseDown: function(event) {
// Prevent text selection in the case of shift-click or double-click.
event.preventDefault();
if (!event.shiftKey) {
return;
}
let swatch = event.target;
let color = this.colorSwatches.get(swatch);
let val = color.nextColorUnit();
swatch.nextElementSibling.textContent = val;
},
+ _onAngleSwatchMouseDown: function(event) {
+ // Prevent text selection in the case of shift-click or double-click.
+ event.preventDefault();
+
+ if (!event.shiftKey) {
+ return;
+ }
+
+ let swatch = event.target;
+ let angle = this.angleSwatches.get(swatch);
+ let val = angle.nextAngleUnit();
+
+ swatch.nextElementSibling.textContent = val;
+ },
+
/**
* A helper function that sanitizes a possibly-unterminated URL.
*/
_sanitizeURL: function(url) {
// Re-lex the URL and add any needed termination characters.
let urlTokenizer = DOMUtils.getCSSLexer(url);
// Just read until EOF; there will only be a single token.
while (urlTokenizer.nextToken()) {
@@ -538,16 +631,19 @@ OutputParser.prototype = {
* - defaultColorType: true // Convert colors to the default type
* // selected in the options panel.
* - colorSwatchClass: "" // The class to use for color swatches.
* - colorClass: "" // The class to use for the color value
* // that follows the swatch.
* - bezierSwatchClass: "" // The class to use for bezier swatches.
* - bezierClass: "" // The class to use for the bezier value
* // that follows the swatch.
+ * - angleSwatchClass: "" // The class to use for angle swatches.
+ * - angleClass: "" // The class to use for the angle value
+ * // that follows the swatch.
* - supportsColor: false // Does the CSS property support colors?
* - urlClass: "" // The class to be used for url() links.
* - baseURI: "" // A string or nsIURI used to resolve
* // relative links.
* - filterSwatch: false // A special case for parsing a
* // "filter" property, causing the
* // parser to skip the call to
* // _wrapFilter. Used only for
@@ -557,16 +653,18 @@ OutputParser.prototype = {
*/
_mergeOptions: function(overrides) {
let defaults = {
defaultColorType: true,
colorSwatchClass: "",
colorClass: "",
bezierSwatchClass: "",
bezierClass: "",
+ angleSwatchClass: "",
+ angleClass: "",
supportsColor: false,
urlClass: "",
baseURI: "",
filterSwatch: false
};
if (typeof overrides.baseURI === "string") {
overrides.baseURI = Services.io.newURI(overrides.baseURI, null, null);
--- a/devtools/client/shared/test/browser.ini
+++ b/devtools/client/shared/test/browser.ini
@@ -14,16 +14,17 @@ support-files =
html-mdn-css-no-summary.html
html-mdn-css-no-summary-or-syntax.html
html-mdn-css-no-syntax.html
html-mdn-css-syntax-old-style.html
leakhunt.js
test-actor.js
test-actor-registry.js
+[browser_css_angle.js]
[browser_css_color.js]
[browser_cubic-bezier-01.js]
[browser_cubic-bezier-02.js]
[browser_cubic-bezier-03.js]
[browser_cubic-bezier-04.js]
[browser_cubic-bezier-05.js]
[browser_cubic-bezier-06.js]
[browser_filter-editor-01.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/shared/test/browser_css_angle.js
@@ -0,0 +1,126 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+/* import-globals-from head.js */
+"use strict";
+
+const TEST_URI = "data:text/html;charset=utf-8,browser_css_angle.js";
+var {angleUtils} = require("devtools/shared/css-angle");
+
+add_task(function*() {
+ yield addTab("about:blank");
+ let [host] = yield createHost("bottom", TEST_URI);
+
+ info("Starting the test");
+ testAngleUtils();
+
+ host.destroy();
+ gBrowser.removeCurrentTab();
+});
+
+function testAngleUtils() {
+ let data = getTestData();
+
+ for (let {authored, deg, rad, grad, turn} of data) {
+ let angle = new angleUtils.CssAngle(authored);
+
+ // Check all values.
+ info("Checking values for " + authored);
+ is(angle.deg, deg, "color.deg === deg");
+ is(angle.rad, rad, "color.rad === rad");
+ is(angle.grad, grad, "color.grad === grad");
+ is(angle.turn, turn, "color.turn === turn");
+
+ testToString(angle, deg, rad, grad, turn);
+ }
+}
+
+function testToString(angle, deg, rad, grad, turn) {
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.deg;
+ is(angle.toString(), deg, "toString() with deg type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.rad;
+ is(angle.toString(), rad, "toString() with rad type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.grad;
+ is(angle.toString(), grad, "toString() with grad type");
+
+ angle.angleUnit = angleUtils.CssAngle.ANGLEUNIT.turn;
+ is(angle.toString(), turn, "toString() with turn type");
+}
+
+function getTestData() {
+ return [{
+ authored: "0deg",
+ deg: "0deg",
+ rad: "0rad",
+ grad: "0grad",
+ turn: "0turn"
+ }, {
+ authored: "180deg",
+ deg: "180deg",
+ rad: "3.14rad",
+ grad: "200grad",
+ turn: "0.5turn"
+ }, {
+ authored: "180DEG",
+ deg: "180DEG",
+ rad: "3.14RAD",
+ grad: "200GRAD",
+ turn: "0.5TURN"
+ }, {
+ authored: `-${Math.PI}rad`,
+ deg: "-180deg",
+ rad: `-${Math.PI}rad`,
+ grad: "-200grad",
+ turn: "-0.5turn"
+ }, {
+ authored: `-${Math.PI}RAD`,
+ deg: "-180DEG",
+ rad: `-${Math.PI}RAD`,
+ grad: "-200GRAD",
+ turn: "-0.5TURN"
+ }, {
+ authored: "100grad",
+ deg: "90deg",
+ rad: "1.57rad",
+ grad: "100grad",
+ turn: "0.25turn"
+ }, {
+ authored: "100GRAD",
+ deg: "90DEG",
+ rad: "1.57RAD",
+ grad: "100GRAD",
+ turn: "0.25TURN"
+ }, {
+ authored: "-1turn",
+ deg: "-360deg",
+ rad: "-6.28rad",
+ grad: "-400grad",
+ turn: "-1turn"
+ }, {
+ authored: "-10TURN",
+ deg: "-3600DEG",
+ rad: "-62.83RAD",
+ grad: "-4000GRAD",
+ turn: "-10TURN"
+ }, {
+ authored: "inherit",
+ deg: "inherit",
+ rad: "inherit",
+ grad: "inherit",
+ turn: "inherit"
+ }, {
+ authored: "initial",
+ deg: "initial",
+ rad: "initial",
+ grad: "initial",
+ turn: "initial"
+ }, {
+ authored: "unset",
+ deg: "unset",
+ rad: "unset",
+ grad: "unset",
+ turn: "unset"
+ }];
+}
--- a/devtools/client/shared/test/browser_outputparser.js
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -17,16 +17,17 @@ function* performTest() {
let [host, , doc] = yield createHost("bottom", "data:text/html," +
"<h1>browser_outputParser.js</h1><div></div>");
let parser = new OutputParser(doc);
testParseCssProperty(doc, parser);
testParseCssVar(doc, parser);
testParseURL(doc, parser);
testParseFilter(doc, parser);
+ testParseAngle(doc, parser);
host.destroy();
}
// Class name used in color swatch.
var COLOR_TEST_CLASS = "test-class";
// Create a new CSS color-parsing test. |name| is the name of the CSS
@@ -96,18 +97,18 @@ function testParseCssProperty(doc, parse
"blur(1px) drop-shadow(0 0 0 ",
{name: "blue"},
") url(red.svg#blue)</span></span>"]),
makeColorTest("color", "currentColor", ["currentColor"]),
// Test a very long property.
makeColorTest("background-image",
- "linear-gradient(0deg, transparent 0, transparent 5%,#F00 0, #F00 10%,#FF0 0, #FF0 15%,#0F0 0, #0F0 20%,#0FF 0, #0FF 25%,#00F 0, #00F 30%,#800 0, #800 35%,#880 0, #880 40%,#080 0, #080 45%,#088 0, #088 50%,#008 0, #008 55%,#FFF 0, #FFF 60%,#EEE 0, #EEE 65%,#CCC 0, #CCC 70%,#999 0, #999 75%,#666 0, #666 80%,#333 0, #333 85%,#111 0, #111 90%,#000 0, #000 95%,transparent 0, transparent 100%)",
- ["linear-gradient(0deg, ", {name: "transparent"},
+ "linear-gradient(to left, transparent 0, transparent 5%,#F00 0, #F00 10%,#FF0 0, #FF0 15%,#0F0 0, #0F0 20%,#0FF 0, #0FF 25%,#00F 0, #00F 30%,#800 0, #800 35%,#880 0, #880 40%,#080 0, #080 45%,#088 0, #088 50%,#008 0, #008 55%,#FFF 0, #FFF 60%,#EEE 0, #EEE 65%,#CCC 0, #CCC 70%,#999 0, #999 75%,#666 0, #666 80%,#333 0, #333 85%,#111 0, #111 90%,#000 0, #000 95%,transparent 0, transparent 100%)",
+ ["linear-gradient(to left, ", {name: "transparent"},
" 0, ", {name: "transparent"},
" 5%,", {name: "#F00"},
" 0, ", {name: "#F00"},
" 10%,", {name: "#FF0"},
" 0, ", {name: "#FF0"},
" 15%,", {name: "#0F0"},
" 0, ", {name: "#0F0"},
" 20%,", {name: "#0FF"},
@@ -254,8 +255,25 @@ function testParseFilter(doc, parser) {
let frag = parser.parseCssProperty("filter", "something invalid", {
filterSwatchClass: "test-filterswatch"
});
let swatchCount = frag.querySelectorAll(".test-filterswatch").length;
is(swatchCount, 1, "filter swatch was created");
}
+function testParseAngle(doc, parser) {
+ let frag = parser.parseCssProperty("image-orientation", "90deg", {
+ angleSwatchClass: "test-angleswatch"
+ });
+
+ let swatchCount = frag.querySelectorAll(".test-angleswatch").length;
+ is(swatchCount, 1, "angle swatch was created");
+
+ frag = parser.parseCssProperty("background-image",
+ "linear-gradient(90deg, red, blue", {
+ angleSwatchClass: "test-angleswatch"
+ });
+
+ swatchCount = frag.querySelectorAll(".test-angleswatch").length;
+ is(swatchCount, 1, "angle swatch was created");
+}
+
new file mode 100644
--- /dev/null
+++ b/devtools/client/themes/images/angle-swatch.svg
@@ -0,0 +1,17 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!-- 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/. -->
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 12 12" width="12px" height="12px">
+ <mask id="angle-mask">
+ <rect width="100%" height="100%" fill="#fff"/>
+ <polygon points="6 6, 12 12, 0 12, 0 0, 6 0, 6 6"/>
+ </mask>
+ <mask id="circle-mask">
+ <circle cx="6" cy="6" r="6" fill="#fff"/>
+ </mask>
+ <circle cx="6" cy="6" r="6" fill="#fff"/>
+ <circle cx="6" cy="6" r="6" mask="url(#angle-mask)" fill="#aeb0b1"/>
+ <line x1="6" y1="0" x2="6" y2="6" stroke-width="0.5" stroke="rgba(0,0,0,0.5)"></line>
+ <line x1="6" y1="6" x2="12" y2="12" stroke-width="0.5" stroke="rgba(0,0,0,0.5)" mask="url(#circle-mask)"></line>
+</svg>
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -315,16 +315,21 @@
background-size: 1em;
}
.ruleview-filterswatch {
background: url("chrome://devtools/skin/images/filter-swatch.svg");
background-size: 1em;
}
+.ruleview-angleswatch {
+ background: url("chrome://devtools/skin/images/angle-swatch.svg");
+ background-size: 1em;
+}
+
@media (min-resolution: 1.1dppx) {
.ruleview-bezierswatch {
background: url("chrome://devtools/skin/images/cubic-bezier-swatch@2x.png");
background-size: 1em;
}
}
.ruleview-overridden {
new file mode 100644
--- /dev/null
+++ b/devtools/shared/css-angle.js
@@ -0,0 +1,348 @@
+/* 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 {Cc, Ci} = require("chrome");
+
+const SPECIALVALUES = new Set([
+ "initial",
+ "inherit",
+ "unset"
+]);
+
+/**
+ * This module is used to convert between various angle units.
+ *
+ * Usage:
+ * let {require} = Cu.import("resource://devtools/shared/Loader.jsm", {});
+ * let {angleUtils} = require("devtools/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 = {
+ "deg": "deg",
+ "rad": "rad",
+ "grad": "grad",
+ "turn": "turn"
+};
+
+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() {
+ return /^-?\d+\.?\d*(deg|rad|grad|turn)$/gi.test(this.authored);
+ },
+
+ 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 * 100) / 100}${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;
+}
+
+loader.lazyGetter(this, "DOMUtils", function() {
+ return Cc["@mozilla.org/inspector/dom-utils;1"].getService(Ci.inIDOMUtils);
+});
--- a/devtools/shared/moz.build
+++ b/devtools/shared/moz.build
@@ -34,16 +34,17 @@ MOCHITEST_CHROME_MANIFESTS += ['tests/mo
XPCSHELL_TESTS_MANIFESTS += ['tests/unit/xpcshell.ini']
JAR_MANIFESTS += ['jar.mn']
DevToolsModules(
'async-storage.js',
'async-utils.js',
'content-observer.js',
+ 'css-angle.js',
'css-color.js',
'deprecated-sync-thenables.js',
'DevToolsUtils.js',
'event-emitter.js',
'event-parsers.js',
'indentation.js',
'Loader.jsm',
'Parser.jsm',
new file mode 100644
--- /dev/null
+++ b/devtools/shared/tests/unit/test_cssAngle.js
@@ -0,0 +1,30 @@
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Test classifyAngle.
+
+"use strict";
+
+const {angleUtils} = require("devtools/shared/css-angle");
+
+const CLASSIFY_TESTS = [
+ { input: "180deg", output: "deg" },
+ { input: "-180deg", output: "deg" },
+ { input: "180DEG", output: "deg" },
+ { input: "200rad", output: "rad" },
+ { input: "-200rad", output: "rad" },
+ { input: "200RAD", output: "rad" },
+ { input: "0.5grad", output: "grad" },
+ { input: "-0.5grad", output: "grad" },
+ { input: "0.5GRAD", output: "grad" },
+ { input: "0.33turn", output: "turn" },
+ { input: "0.33TURN", output: "turn" },
+ { input: "-0.33turn", output: "turn" }
+];
+
+function run_test() {
+ for (let test of CLASSIFY_TESTS) {
+ let result = angleUtils.classifyAngle(test.input);
+ equal(result, test.output, "test classifyAngle(" + test.input + ")");
+ }
+}
--- a/devtools/shared/tests/unit/xpcshell.ini
+++ b/devtools/shared/tests/unit/xpcshell.ini
@@ -14,14 +14,15 @@ support-files =
[test_fetch-resource.js]
[test_indentation.js]
[test_independent_loaders.js]
[test_invisible_loader.js]
[test_safeErrorString.js]
[test_defineLazyPrototypeGetter.js]
[test_async-utils.js]
[test_consoleID.js]
+[test_cssAngle.js]
[test_cssColor.js]
[test_prettifyCSS.js]
[test_require_lazy.js]
[test_require.js]
[test_stack.js]
[test_executeSoon.js]