--- a/devtools/client/inspector/rules/models/element-style.js
+++ b/devtools/client/inspector/rules/models/element-style.js
@@ -5,17 +5,17 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const promise = require("promise");
const Rule = require("devtools/client/inspector/rules/models/rule");
const {promiseWarn} = require("devtools/client/inspector/shared/utils");
const {ELEMENT_STYLE} = require("devtools/shared/specs/styles");
-const {getCssProperties} = require("devtools/shared/fronts/css-properties");
+const {getCssProperties, isCssVariable} = require("devtools/shared/fronts/css-properties");
/**
* ElementStyle is responsible for the following:
* Keeps track of which properties are overridden.
* Maintains a list of Rule objects for a given element.
*
* @param {Element} element
* The element whose style we are viewing.
@@ -35,16 +35,17 @@ function ElementStyle(element, ruleView,
showUserAgentStyles) {
this.element = element;
this.ruleView = ruleView;
this.store = store || {};
this.pageStyle = pageStyle;
this.showUserAgentStyles = showUserAgentStyles;
this.rules = [];
this.cssProperties = getCssProperties(this.ruleView.inspector.toolbox);
+ this.variables = new Map();
// We don't want to overwrite this.store.userProperties so we only create it
// if it doesn't already exist.
if (!("userProperties" in this.store)) {
this.store.userProperties = new UserProperties();
}
if (!("disabled" in this.store)) {
@@ -194,17 +195,19 @@ ElementStyle.prototype = {
this.rules.push(rule);
return true;
},
/**
* Calls markOverridden with all supported pseudo elements
*/
markOverriddenAll: function () {
+ this.variables.clear();
this.markOverridden();
+
for (let pseudo of this.cssProperties.pseudoElements) {
this.markOverridden(pseudo);
}
},
/**
* Mark the properties listed in this.rules for a given pseudo element
* with an overridden flag if an earlier property overrides it.
@@ -283,16 +286,20 @@ ElementStyle.prototype = {
overridden = !!earlier;
}
computedProp._overriddenDirty =
(!!computedProp.overridden !== overridden);
computedProp.overridden = overridden;
if (!computedProp.overridden && computedProp.textProp.enabled) {
taken[computedProp.name] = computedProp;
+
+ if (isCssVariable(computedProp.name)) {
+ this.variables.set(computedProp.name, computedProp.value);
+ }
}
}
// For each TextProperty, mark it overridden if all of its
// computed properties are marked overridden. Update the text
// property's associated editor, if any. This will clear the
// _overriddenDirty state on all computed properties.
for (let textProp of textProps) {
@@ -323,17 +330,30 @@ ElementStyle.prototype = {
}
dirty = computedProp._overriddenDirty || dirty;
delete computedProp._overriddenDirty;
}
dirty = (!!prop.overridden !== overridden) || dirty;
prop.overridden = overridden;
return dirty;
- }
+ },
+
+ /**
+ * Returns the current value of a CSS variable; or null if the
+ * variable is not defined.
+ *
+ * @param {String} name
+ * The name of the variable.
+ * @return {String} the variable's value or null if the variable is
+ * not defined.
+ */
+ getVariable: function (name) {
+ return this.variables.get(name);
+ },
};
/**
* Store of CSSStyleDeclarations mapped to properties that have been changed by
* the user.
*/
function UserProperties() {
this.map = new Map();
--- a/devtools/client/inspector/rules/test/browser.ini
+++ b/devtools/client/inspector/rules/test/browser.ini
@@ -31,16 +31,18 @@ support-files =
doc_sourcemaps.scss
doc_sourcemaps2.css
doc_sourcemaps2.css^headers^
doc_sourcemaps2.html
doc_style_editor_link.css
doc_test_image.png
doc_urls_clickable.css
doc_urls_clickable.html
+ doc_variables_1.html
+ doc_variables_2.html
head.js
!/devtools/client/commandline/test/helpers.js
!/devtools/client/framework/test/shared-head.js
!/devtools/client/inspector/test/head.js
!/devtools/client/inspector/test/shared-head.js
!/devtools/client/shared/test/test-actor.js
!/devtools/client/shared/test/test-actor-registry.js
@@ -91,16 +93,18 @@ skip-if = stylo # Bug 1387445
[browser_rules_completion-new-property_03.js]
[browser_rules_completion-new-property_04.js]
[browser_rules_completion-new-property_multiline.js]
[browser_rules_computed-lists_01.js]
[browser_rules_computed-lists_02.js]
[browser_rules_completion-popup-hidden-after-navigation.js]
[browser_rules_content_01.js]
[browser_rules_content_02.js]
+[browser_rules_variables_01.js]
+[browser_rules_variables_02.js]
skip-if = e10s && debug # Bug 1250058 - Docshell leak on debug e10s
[browser_rules_context-menu-show-mdn-docs-01.js]
[browser_rules_context-menu-show-mdn-docs-02.js]
[browser_rules_context-menu-show-mdn-docs-03.js]
[browser_rules_copy_styles.js]
subsuite = clipboard
skip-if = (os == 'linux' && bits == 32 && debug) # bug 1328915, disable linux32 debug devtools for timeouts
[browser_rules_cssom.js]
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_variables_01.js
@@ -0,0 +1,35 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for variables in rule view.
+
+const TEST_URI = URL_ROOT + "doc_variables_1.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+ yield selectNode("#target", inspector);
+
+ info("Tests basic support for CSS Variables for both single variable " +
+ "and double variable. Formats tested: var(x, constant), var(x, var(y))");
+
+ let unsetColor = getRuleViewProperty(view, "div", "color").valueSpan
+ .querySelector(".ruleview-variable-unmatched");
+ let setColor = unsetColor.previousElementSibling;
+ is(unsetColor.textContent, " red", "red is unmatched in color");
+ is(setColor.textContent, "--color", "--color is not set correctly");
+ is(setColor.title, "--color = chartreuse", "--color's title is not set correctly");
+
+ let unsetVar = getRuleViewProperty(view, "div", "background-color").valueSpan
+ .querySelector(".ruleview-variable-unmatched");
+ let setVar = unsetVar.nextElementSibling;
+ let setVarName = setVar.firstElementChild.firstElementChild;
+ is(unsetVar.textContent, "--not-set",
+ "--not-set is unmatched in background-color");
+ is(setVar.textContent, " var(--bg)", "var(--bg) parsed incorrectly");
+ is(setVarName.textContent, "--bg", "--bg is not set correctly");
+ is(setVarName.title, "--bg = seagreen", "--bg's title is not set correctly");
+});
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/browser_rules_variables_02.js
@@ -0,0 +1,190 @@
+/* vim: set ft=javascript ts=2 et sw=2 tw=80: */
+/* Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ */
+
+"use strict";
+
+// Test for variables in rule view.
+
+const TEST_URI = URL_ROOT + "doc_variables_2.html";
+
+add_task(function* () {
+ yield addTab(TEST_URI);
+ let {inspector, view} = yield openRuleView();
+
+ yield testBasic(inspector, view);
+ yield testNestedCssFunctions(inspector, view);
+ yield testBorderShorthandAndInheritance(inspector, view);
+ yield testSingleLevelVariable(inspector, view);
+ yield testDoubleLevelVariable(inspector, view);
+ yield testTripleLevelVariable(inspector, view);
+});
+
+function* testBasic(inspector, view) {
+ info("Test support for basic variable functionality for var() with 2 variables." +
+ "Format: var(--var1, var(--var2))");
+
+ yield selectNode("#a", inspector);
+ let unsetVar = getRuleViewProperty(view, "#a", "font-size").valueSpan
+ .querySelector(".ruleview-variable-unmatched");
+ let setVarParent = unsetVar.nextElementSibling;
+ let setVar = getVarFromParent(setVarParent);
+ is(unsetVar.textContent, "--var-not-defined",
+ "--var-not-defined is not set correctly");
+ is(unsetVar.title, "--var-not-defined is not set",
+ "--var-not-defined's title is not set correctly");
+ is(setVarParent.textContent, " var(--var-defined-font-size)",
+ "var(--var-defined-font-size) parsed incorrectly");
+ is(setVar.textContent, "--var-defined-font-size",
+ "--var-defined-font-size is not set correctly");
+ is(setVar.title, "--var-defined-font-size = 60px",
+ "--bg's title is not set correctly");
+}
+
+function* testNestedCssFunctions(inspector, view) {
+ info("Test support for variable functionality for a var() nested inside " +
+ "another CSS function. Format: rgb(0, 0, var(--var1, var(--var2)))");
+
+ yield selectNode("#b", inspector);
+ let unsetVarParent = getRuleViewProperty(view, "#b", "color").valueSpan
+ .querySelector(".ruleview-variable-unmatched");
+ let unsetVar = getVarFromParent(unsetVarParent);
+ let setVar = unsetVarParent.previousElementSibling;
+ is(unsetVarParent.textContent, " var(--var-defined-r-2)",
+ "var(--var-defined-r-2) not parsed correctly");
+ is(unsetVar.textContent, "--var-defined-r-2",
+ "--var-defined-r-2 is not set correctly");
+ is(unsetVar.title, "--var-defined-r-2 = 0",
+ "--var-defined-r-2's title is not set correctly");
+ is(setVar.textContent, "--var-defined-r-1",
+ "--var-defined-r-1 is not set correctly");
+ is(setVar.title, "--var-defined-r-1 = 255",
+ "--var-defined-r-1's title is not set correctly");
+}
+
+function* testBorderShorthandAndInheritance(inspector, view) {
+ info("Test support for variable functionality for shorthands/CSS styles with spaces " +
+ "like \"margin: w x y z\". Also tests functionality for inherticance of CSS" +
+ " variables. Format: var(l, var(m)) var(x) rgb(var(r) var(g) var(b))");
+
+ yield selectNode("#c", inspector);
+ let unsetVarL = getRuleViewProperty(view, "#c", "border").valueSpan
+ .querySelector(".ruleview-variable-unmatched");
+ let setVarMParent = unsetVarL.nextElementSibling;
+
+ // var(x) is the next sibling of the parent of M
+ let setVarXParent = setVarMParent.parentNode.nextElementSibling;
+
+ // var(r) is the next sibling of var(x), and var(g) is the next sibling of var(r), etc.
+ let setVarRParent = setVarXParent.nextElementSibling;
+ let setVarGParent = setVarRParent.nextElementSibling;
+ let setVarBParent = setVarGParent.nextElementSibling;
+
+ let setVarM = getVarFromParent(setVarMParent);
+ let setVarX = setVarXParent.firstElementChild;
+ let setVarR = setVarRParent.firstElementChild;
+ let setVarG = setVarGParent.firstElementChild;
+ let setVarB = setVarBParent.firstElementChild;
+
+ is(unsetVarL.textContent, "--var-undefined",
+ "--var-undefined is not set correctly");
+ is(unsetVarL.title, "--var-undefined is not set",
+ "--var-undefined's title is not set correctly");
+
+ is(setVarM.textContent, "--var-border-px",
+ "--var-border-px is not set correctly");
+ is(setVarM.title, "--var-border-px = 10px",
+ "--var-border-px's title is not set correctly");
+
+ is(setVarX.textContent, "--var-border-style",
+ "--var-border-style is not set correctly");
+ is(setVarX.title, "--var-border-style = solid",
+ "var-border-style's title is not set correctly");
+
+ is(setVarR.textContent, "--var-border-r",
+ "--var-defined-r is not set correctly");
+ is(setVarR.title, "--var-border-r = 255",
+ "--var-defined-r's title is not set correctly");
+
+ is(setVarG.textContent, "--var-border-g",
+ "--var-defined-g is not set correctly");
+ is(setVarG.title, "--var-border-g = 0",
+ "--var-defined-g's title is not set correctly");
+
+ is(setVarB.textContent, "--var-border-b",
+ "--var-defined-b is not set correctly");
+ is(setVarB.title, "--var-border-b = 0",
+ "--var-defined-b's title is not set correctly");
+}
+
+function* testSingleLevelVariable(inspector, view) {
+ info("Test support for variable functionality of a single level of " +
+ "undefined variables. Format: var(x, constant)");
+
+ yield selectNode("#d", inspector);
+ let unsetVar = getRuleViewProperty(view, "#d", "font-size").valueSpan
+ .querySelector(".ruleview-variable-unmatched");
+
+ is(unsetVar.textContent, "--var-undefined",
+ "--var-undefined is not set correctly");
+ is(unsetVar.title, "--var-undefined is not set",
+ "--var-undefined's title is not set correctly");
+}
+
+function* testDoubleLevelVariable(inspector, view) {
+ info("Test support for variable functionality of double level of " +
+ "undefined variables. Format: var(x, var(y, constant))");
+
+ yield selectNode("#e", inspector);
+ let allUnsetVars = getRuleViewProperty(view, "#e", "color").valueSpan
+ .querySelectorAll(".ruleview-variable-unmatched");
+
+ is(allUnsetVars.length, 2, "The number of unset variables is mismatched.");
+
+ let unsetVar1 = allUnsetVars[0];
+ let unsetVar2 = allUnsetVars[1];
+
+ is(unsetVar1.textContent, "--var-undefined",
+ "--var-undefined is not set correctly");
+ is(unsetVar1.title, "--var-undefined is not set",
+ "--var-undefined's title is not set correctly");
+
+ is(unsetVar2.textContent, "--var-undefined-2",
+ "--var-undefined is not set correctly");
+ is(unsetVar2.title, "--var-undefined-2 is not set",
+ "--var-undefined-2's title is not set correctly");
+}
+
+function* testTripleLevelVariable(inspector, view) {
+ info("Test support for variable functionality of triple level of " +
+ "undefined variables. Format: var(x, var(y, var(z, constant)))");
+
+ yield selectNode("#f", inspector);
+ let allUnsetVars = getRuleViewProperty(view, "#f", "border-style").valueSpan
+ .querySelectorAll(".ruleview-variable-unmatched");
+
+ is(allUnsetVars.length, 3, "The number of unset variables is mismatched.");
+
+ let unsetVar1 = allUnsetVars[0];
+ let unsetVar2 = allUnsetVars[1];
+ let unsetVar3 = allUnsetVars[2];
+
+ is(unsetVar1.textContent, "--var-undefined",
+ "--var-undefined is not set correctly");
+ is(unsetVar1.title, "--var-undefined is not set",
+ "--var-undefined's title is not set correctly");
+
+ is(unsetVar2.textContent, "--var-undefined-2",
+ "--var-undefined-2 is not set correctly");
+ is(unsetVar2.title, "--var-undefined-2 is not set",
+ "--var-defined-r-2's title is not set correctly");
+
+ is(unsetVar3.textContent, "--var-undefined-3",
+ "--var-undefined-3 is not set correctly");
+ is(unsetVar3.title, "--var-undefined-3 is not set",
+ "--var-defined-r-3's title is not set correctly");
+}
+
+function getVarFromParent(varParent) {
+ return varParent.firstElementChild.firstElementChild;
+}
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_variables_1.html
@@ -0,0 +1,23 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>variables test</title>
+
+ <style>
+ * {
+ --color: tomato;
+ --bg: violet;
+ }
+
+ div {
+ --color: chartreuse;
+ color: var(--color, red);
+ background-color: var(--not-set, var(--bg));
+ }
+ </style>
+</head>
+<body>
+ <div id="target" style="--bg: seagreen;"> the ocean </div>
+</body>
+</html>
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/rules/test/doc_variables_2.html
@@ -0,0 +1,45 @@
+<!-- Any copyright is dedicated to the Public Domain.
+ http://creativecommons.org/publicdomain/zero/1.0/ -->
+<html>
+<head>
+ <title>variables test</title>
+ <style>
+ :root {
+ --var-border-px: 10px;
+ --var-border-style: solid;
+ --var-border-r: 255;
+ --var-border-g: 0;
+ --var-border-b: 0;
+ }
+ #a {
+ --var-defined-font-size: 60px;
+ font-size: var(--var-not-defined, var(--var-defined-font-size));
+ }
+ #b {
+ --var-defined-r-1: 255;
+ --var-defined-r-2: 0;
+ color: rgb(var(--var-defined-r-1, var(--var-defined-r-2)), 0, 0);
+ }
+ #c {
+ border: var(--var-undefined, var(--var-border-px)) var(--var-border-style) rgb(var(--var-border-r), var(--var-border-g), var(--var-border-b))
+ }
+ #d {
+ font-size: var(--var-undefined, 30px);
+ }
+ #e {
+ color: var(--var-undefined, var(--var-undefined-2, blue));
+ }
+ #f {
+ border-style: var(--var-undefined, var(--var-undefined-2, var(--var-undefined-3, solid)));
+ }
+ </style>
+</head>
+<body>
+ <div id="a">A</div><br>
+ <div id="b">B</div><br>
+ <div id="c">C</div><br>
+ <div id="d">D</div><br>
+ <div id="e">E</div><br>
+ <div id="f">F</div>
+</body>
+</html>
--- a/devtools/client/inspector/rules/views/text-property-editor.js
+++ b/devtools/client/inspector/rules/views/text-property-editor.js
@@ -358,17 +358,19 @@ TextPropertyEditor.prototype = {
colorClass: "ruleview-color",
colorSwatchClass: SHARED_SWATCH_CLASS + " " + COLOR_SWATCH_CLASS,
filterClass: "ruleview-filter",
filterSwatchClass: SHARED_SWATCH_CLASS + " " + FILTER_SWATCH_CLASS,
gridClass: "ruleview-grid",
shapeClass: "ruleview-shape",
defaultColorType: !propDirty,
urlClass: "theme-link",
- baseURI: this.sheetHref
+ baseURI: this.sheetHref,
+ unmatchedVariableClass: "ruleview-variable-unmatched",
+ isVariableInUse: varName => this.rule.elementStyle.getVariable(varName),
};
let frag = outputParser.parseCssProperty(name, val, parserOptions);
this.valueSpan.innerHTML = "";
this.valueSpan.appendChild(frag);
this.ruleView.emit("property-value-updated", this.valueSpan);
// Attach the color picker tooltip to the color swatches
--- a/devtools/client/shared/output-parser.js
+++ b/devtools/client/shared/output-parser.js
@@ -13,16 +13,20 @@ const {
BASIC_SHAPE_FUNCTIONS,
BEZIER_KEYWORDS,
COLOR_TAKING_FUNCTIONS,
CSS_TYPES
} = require("devtools/shared/css/properties-db");
const {appendText} = require("devtools/client/inspector/shared/utils");
const Services = require("Services");
+const STYLE_INSPECTOR_PROPERTIES = "devtools/shared/locales/styleinspector.properties";
+const {LocalizationHelper} = require("devtools/shared/l10n");
+const STYLE_INSPECTOR_L10N = new LocalizationHelper(STYLE_INSPECTOR_PROPERTIES);
+
const HTML_NS = "http://www.w3.org/1999/xhtml";
const CSS_GRID_ENABLED_PREF = "layout.css.grid.enabled";
const CSS_SHAPES_ENABLED_PREF = "devtools.inspector.shapesHighlighter.enabled";
/**
* 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
@@ -91,92 +95,215 @@ OutputParser.prototype = {
return this._parse(value, options);
}
this._appendTextNode(value);
return this._toDOM();
},
/**
- * Given an initial FUNCTION token, read tokens from |tokenStream|
- * and collect all the (non-comment) text. Return the collected
- * text. The function token and the close paren are included in the
- * result.
+ * Read tokens from |tokenStream| and collect all the (non-comment)
+ * text. Return the collected texts and variable data (if any).
+ * Stop when an unmatched closing paren is seen.
+ * If |stopAtComma| is true, then also stop when a top-level
+ * (unparenthesized) comma is seen.
*
- * @param {CSSToken} initialToken
- * The FUNCTION token.
* @param {String} text
- * The original CSS text.
+ * The original source text.
* @param {CSSLexer} tokenStream
* The token stream from which to read.
- * @return {String}
- * The text of body of the function call.
+ * @param {Object} options
+ * The options object in use; @see _mergeOptions.
+ * @param {Boolean} stopAtComma
+ * If true, stop at a comma.
+ * @return {Object}
+ * An object of the form {tokens, functionData, sawComma, sawVariable}.
+ * |tokens| is a list of the non-comment, non-whitespace tokens
+ * that were seen. The stopping token (paren or comma) will not
+ * be included.
+ * |functionData| is a list of parsed strings and nodes that contain the
+ * data between the matching parenthesis. The stopping token's text will
+ * not be included.
+ * |sawComma| is true if the stop was due to a comma, or false otherwise.
+ * |sawVariable| is true if a variable was seen while parsing the text.
*/
- _collectFunctionText: function (initialToken, text, tokenStream) {
- let result = text.substring(initialToken.startOffset,
- initialToken.endOffset);
+ _parseMatchingParens: function (text, tokenStream, options, stopAtComma) {
let depth = 1;
+ let functionData = [];
+ let tokens = [];
+ let sawVariable = false;
+
while (depth > 0) {
let token = tokenStream.nextToken();
if (!token) {
break;
}
if (token.tokenType === "comment") {
continue;
}
- result += text.substring(token.startOffset, token.endOffset);
+
if (token.tokenType === "symbol") {
- if (token.text === "(") {
+ if (stopAtComma && depth === 1 && token.text === ",") {
+ return { tokens, functionData, sawComma: true, sawVariable };
+ } else if (token.text === "(") {
++depth;
} else if (token.text === ")") {
--depth;
+ if (depth === 0) {
+ break;
+ }
}
+ } else if (token.tokenType === "function" && token.text === "var" &&
+ options.isVariableInUse) {
+ sawVariable = true;
+ let variableNode = this._parseVariable(token, text, tokenStream, options);
+ functionData.push(variableNode);
} else if (token.tokenType === "function") {
++depth;
}
+
+ if (token.tokenType !== "function" || token.text !== "var" ||
+ !options.isVariableInUse) {
+ functionData.push(text.substring(token.startOffset, token.endOffset));
+ }
+
+ if (token.tokenType !== "whitespace") {
+ tokens.push(token);
+ }
}
- return result;
+
+ return { tokens, functionData, sawComma: false, sawVariable };
},
/**
- * Parse a string.
+ * Parse var() use and return a variable node to be added to the output state.
+ * This will read tokens up to and including the ")" that closes the "var("
+ * invocation.
+ *
+ * @param {CSSToken} initialToken
+ * The "var(" token that was already seen.
+ * @param {String} text
+ * The original input text.
+ * @param {CSSLexer} tokenStream
+ * The token stream from which to read.
+ * @param {Object} options
+ * The options object in use; @see _mergeOptions.
+ * @return {Object}
+ * A node for the variable, with the appropriate text and
+ * title. Eg. a span with "var(--var1)" as the textContent
+ * and a title for --var1 like "--var1 = 10" or
+ * "--var1 is not set".
+ */
+ _parseVariable: function (initialToken, text, tokenStream, options) {
+ // Handle the "var(".
+ let varText = text.substring(initialToken.startOffset,
+ initialToken.endOffset);
+ let variableNode = this._createNode("span", {}, varText);
+
+ // Parse the first variable name within the parens of var().
+ let {tokens, functionData, sawComma, sawVariable} =
+ this._parseMatchingParens(text, tokenStream, options, true);
+
+ let result = sawVariable ? "" : functionData.join("");
+
+ // Display options for the first and second argument in the var().
+ let firstOpts = {};
+ let secondOpts = {};
+
+ let varValue;
+
+ // Get the variable value if it is in use.
+ if (tokens && tokens.length === 1) {
+ varValue = options.isVariableInUse(tokens[0].text);
+ }
+
+ // Get the variable name.
+ let varName = text.substring(tokens[0].startOffset, tokens[0].endOffset);
+
+ if (typeof varValue === "string") {
+ // The variable value is valid, set the variable name's title of the first argument
+ // in var() to display the variable name and value.
+ firstOpts.title =
+ STYLE_INSPECTOR_L10N.getFormatStr("rule.variableValue", varName, varValue);
+ secondOpts.class = options.unmatchedVariableClass;
+ } else {
+ // The variable name is not valid, mark it unmatched.
+ firstOpts.class = options.unmatchedVariableClass;
+ firstOpts.title = STYLE_INSPECTOR_L10N.getFormatStr("rule.variableUnset",
+ varName);
+ }
+
+ variableNode.appendChild(this._createNode("span", firstOpts, result));
+
+ // If we saw a ",", then append it and show the remainder using
+ // the correct highlighting.
+ if (sawComma) {
+ variableNode.appendChild(this.doc.createTextNode(","));
+
+ // Parse the text up until the close paren, being sure to
+ // disable the special case for filter.
+ let subOptions = Object.assign({}, options);
+ subOptions.expectFilter = false;
+ let saveParsed = this.parsed;
+ this.parsed = [];
+ let rest = this._doParse(text, subOptions, tokenStream, true);
+ this.parsed = saveParsed;
+
+ let span = this._createNode("span", secondOpts);
+ span.appendChild(rest);
+ variableNode.appendChild(span);
+ }
+ variableNode.appendChild(this.doc.createTextNode(")"));
+
+ return variableNode;
+ },
+
+ /* eslint-disable complexity */
+ /**
+ * The workhorse for @see _parse. This parses some CSS text,
+ * stopping at EOF; or optionally when an umatched close paren is
+ * seen.
*
* @param {String} text
- * Text to parse.
- * @param {Object} [options]
- * Options object. For valid options and default values see
- * _mergeOptions().
+ * The original input text.
+ * @param {Object} options
+ * The options object in use; @see _mergeOptions.
+ * @param {CSSLexer} tokenStream
+ * The token stream from which to read
+ * @param {Boolean} stopAtCloseParen
+ * If true, stop at an umatched close paren.
* @return {DocumentFragment}
* A document fragment.
*/
- _parse: function (text, options = {}) {
- text = text.trim();
- this.parsed.length = 0;
-
- let tokenStream = getCSSLexer(text);
- let parenDepth = 0;
+ _doParse: function (text, options, tokenStream, stopAtCloseParen) {
+ let parenDepth = stopAtCloseParen ? 1 : 0;
let outerMostFunctionTakesColor = false;
let colorOK = function () {
return options.supportsColor ||
(options.expectFilter && parenDepth === 1 &&
outerMostFunctionTakesColor);
};
let angleOK = function (angle) {
return (new angleUtils.CssAngle(angle)).valid;
};
let spaceNeeded = false;
- let token = tokenStream.nextToken();
- while (token) {
+ let done = false;
+
+ while (!done) {
+ let token = tokenStream.nextToken();
+ if (!token) {
+ break;
+ }
+
if (token.tokenType === "comment") {
// This doesn't change spaceNeeded, because we didn't emit
// anything to the output.
- token = tokenStream.nextToken();
continue;
}
switch (token.tokenType) {
case "function": {
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
@@ -185,31 +312,54 @@ OutputParser.prototype = {
// can be handled in a single place.
this._appendTextNode(text.substring(token.startOffset,
token.endOffset));
if (parenDepth === 0) {
outerMostFunctionTakesColor = COLOR_TAKING_FUNCTIONS.includes(
token.text);
}
++parenDepth;
+ } else if (token.text === "var" && options.isVariableInUse) {
+ let variableNode = this._parseVariable(token, text, tokenStream, options);
+ this.parsed.push(variableNode);
} else {
- let functionText = this._collectFunctionText(token, text,
- tokenStream);
+ let {functionData, sawVariable} = this._parseMatchingParens(text, tokenStream,
+ options);
+
+ let functionName = text.substring(token.startOffset, token.endOffset);
- if (options.expectCubicBezier && token.text === "cubic-bezier") {
- this._appendCubicBezier(functionText, options);
- } else if (colorOK() &&
- colorUtils.isValidCSSColor(functionText, this.cssColor4)) {
- this._appendColor(functionText, options);
- } else if (options.expectShape &&
- Services.prefs.getBoolPref(CSS_SHAPES_ENABLED_PREF) &&
- BASIC_SHAPE_FUNCTIONS.includes(token.text)) {
- this._appendShape(functionText, options);
+ if (sawVariable) {
+ // If function contains variable, we need to add both strings
+ // and nodes.
+ this._appendTextNode(functionName);
+ for (let data of functionData) {
+ if (typeof data === "string") {
+ this._appendTextNode(data);
+ } else if (data) {
+ this.parsed.push(data);
+ }
+ }
+ this._appendTextNode(")");
} else {
- this._appendTextNode(functionText);
+ // If no variable in function, join the text together and add
+ // to DOM accordingly.
+ let functionText = functionName + functionData.join("") + ")";
+
+ if (options.expectCubicBezier && token.text === "cubic-bezier") {
+ this._appendCubicBezier(functionText, options);
+ } else if (colorOK() &&
+ colorUtils.isValidCSSColor(functionText, this.cssColor4)) {
+ this._appendColor(functionText, options);
+ } else if (options.expectShape &&
+ Services.prefs.getBoolPref(CSS_SHAPES_ENABLED_PREF) &&
+ BASIC_SHAPE_FUNCTIONS.includes(token.text)) {
+ this._appendShape(functionText, options);
+ } else {
+ this._appendTextNode(functionText);
+ }
}
}
break;
}
case "ident":
if (options.expectCubicBezier &&
BEZIER_KEYWORDS.indexOf(token.text) >= 0) {
@@ -257,16 +407,22 @@ OutputParser.prototype = {
token.text, options);
break;
case "symbol":
if (token.text === "(") {
++parenDepth;
} else if (token.text === ")") {
--parenDepth;
+
+ if (stopAtCloseParen && parenDepth === 0) {
+ done = true;
+ break;
+ }
+
if (parenDepth === 0) {
outerMostFunctionTakesColor = false;
}
}
// falls through
default:
this._appendTextNode(
text.substring(token.startOffset, token.endOffset));
@@ -274,28 +430,46 @@ OutputParser.prototype = {
}
// If this token might possibly introduce token pasting when
// color-cycling, require a space.
spaceNeeded = (token.tokenType === "ident" || token.tokenType === "at" ||
token.tokenType === "id" || token.tokenType === "hash" ||
token.tokenType === "number" || token.tokenType === "dimension" ||
token.tokenType === "percentage" || token.tokenType === "dimension");
-
- token = tokenStream.nextToken();
}
let result = this._toDOM();
if (options.expectFilter && !options.filterSwatch) {
result = this._wrapFilter(text, options, result);
}
return result;
},
+ /* eslint-enable complexity */
+
+ /**
+ * Parse a string.
+ *
+ * @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 = {}) {
+ text = text.trim();
+ this.parsed.length = 0;
+
+ let tokenStream = getCSSLexer(text);
+ return this._doParse(text, options, tokenStream, false);
+ },
/**
* Return true if it's a display:[inline-]grid token.
*
* @param {String} text
* the parsed text.
* @param {Object} token
* the parsed token.
@@ -1237,16 +1411,24 @@ OutputParser.prototype = {
* // _wrapFilter. Used only for
* // previewing with the filter swatch.
* - gridClass: "" // The class to use for the grid icon.
* - shapeClass: "" // The class to use for the shape icon.
* - supportsColor: false // Does the CSS property support colors?
* - urlClass: "" // The class to be used for url() links.
* - baseURI: undefined // A string used to resolve
* // relative links.
+ * - isVariableInUse // A function taking a single
+ * // argument, the name of a variable.
+ * // This should return the variable's
+ * // value, if it is in use; or null.
+ * - unmatchedVariableClass: ""
+ * // The class to use for a component
+ * // of a "var(...)" that is not in
+ * // use.
* @return {Object}
* Overridden options object
*/
_mergeOptions: function (overrides) {
let defaults = {
defaultColorType: true,
angleClass: "",
angleSwatchClass: "",
@@ -1255,16 +1437,18 @@ OutputParser.prototype = {
colorClass: "",
colorSwatchClass: "",
filterSwatch: false,
gridClass: "",
shapeClass: "",
supportsColor: false,
urlClass: "",
baseURI: undefined,
+ isVariableInUse: null,
+ unmatchedVariableClass: null,
};
for (let item in overrides) {
defaults[item] = overrides[item];
}
return defaults;
}
};
--- a/devtools/client/shared/test/browser_outputparser.js
+++ b/devtools/client/shared/test/browser_outputparser.js
@@ -24,16 +24,17 @@ function* performTest() {
let parser = new OutputParser(doc, cssProperties);
testParseCssProperty(doc, parser);
testParseCssVar(doc, parser);
testParseURL(doc, parser);
testParseFilter(doc, parser);
testParseAngle(doc, parser);
testParseShape(doc, parser);
+ testParseVariable(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
@@ -408,8 +409,50 @@ function testParseShape(doc, parser) {
let frag = parser.parseCssProperty("clip-path", definition, {
shapeClass: "ruleview-shape"
});
let spans = frag.querySelectorAll(".ruleview-shape-point");
is(spans.length, spanCount, desc + " span count");
is(frag.textContent, definition, desc + " text content");
}
}
+
+function testParseVariable(doc, parser) {
+ let TESTS = [
+ {
+ text: "var(--seen)",
+ variables: {"--seen": "chartreuse" },
+ expected: "var(<span title=\"--seen = chartreuse\">--seen</span>)"
+ },
+ {
+ text: "var(--not-seen)",
+ variables: {},
+ expected: "var(<span class=\"unmatched-class\" title=\"--not-seen is not set\">--not-seen</span>)"
+ },
+ {
+ text: "var(--seen, seagreen)",
+ variables: {"--seen": "chartreuse" },
+ expected: "var(<span title=\"--seen = chartreuse\">--seen</span>,<span class=\"unmatched-class\"> <span data-color=\"seagreen\"><span>seagreen</span></span></span>)"
+ },
+ {
+ text: "var(--not-seen, var(--seen))",
+ variables: {"--seen": "chartreuse" },
+ expected: "var(<span class=\"unmatched-class\" title=\"--not-seen is not set\">--not-seen</span>,<span> var(<span title=\"--seen = chartreuse\">--seen</span>)</span>)"
+ },
+ ];
+
+ for (let test of TESTS) {
+ let getValue = function (varName) {
+ return test.variables[varName];
+ };
+
+ let frag = parser.parseCssProperty("color", test.text, {
+ isVariableInUse: getValue,
+ unmatchedVariableClass: "unmatched-class"
+ });
+
+ let target = doc.querySelector("div");
+ target.appendChild(frag);
+
+ is(target.innerHTML, test.expected, test.text);
+ target.innerHTML = "";
+ }
+}
--- a/devtools/client/themes/rules.css
+++ b/devtools/client/themes/rules.css
@@ -561,17 +561,18 @@
}
.ruleview-selectorcontainer {
word-wrap: break-word;
cursor: text;
}
.ruleview-selector-separator,
-.ruleview-selector-unmatched {
+.ruleview-selector-unmatched,
+.ruleview-variable-unmatched {
color: #888;
}
.ruleview-selector-matched > .ruleview-selector-attribute {
/* TODO: Bug 1178535 Awaiting UX feedback on highlight colors */
}
.ruleview-selector-matched > .ruleview-selector-pseudo-class {
--- a/devtools/shared/fronts/css-properties.js
+++ b/devtools/shared/fronts/css-properties.js
@@ -144,17 +144,18 @@ CssProperties.prototype = {
/**
* Checks to see if the property is an inherited one.
*
* @param {String} property The property name to be checked.
* @return {Boolean}
*/
isInherited(property) {
- return this.properties[property] && this.properties[property].isInherited;
+ return (this.properties[property] && this.properties[property].isInherited) ||
+ isCssVariable(property);
},
/**
* Checks if the property supports the given CSS type.
* CSS types should come from devtools/shared/css/properties-db.js' CSS_TYPES.
*
* @param {String} property The property to be checked.
* @param {Number} type One of the type values from CSS_TYPES.
@@ -345,10 +346,11 @@ function reattachCssColorValues(db) {
}
}
module.exports = {
CssPropertiesFront,
CssProperties,
getCssProperties,
getClientCssProperties,
- initCssProperties
+ initCssProperties,
+ isCssVariable,
};
--- a/devtools/shared/locales/en-US/styleinspector.properties
+++ b/devtools/shared/locales/en-US/styleinspector.properties
@@ -62,16 +62,28 @@ rule.warning.title=Invalid property valu
# of the search button that is shown next to a property that has been overridden
# in the rule view.
rule.filterProperty.title=Filter rules containing this property
# LOCALIZATION NOTE (ruleView.empty): Text displayed when the highlighter is
# first opened and there's no node selected in the rule view.
rule.empty=No element selected.
+# LOCALIZATION NOTE (rule.variableValue): Text displayed in a tooltip
+# when the mouse is over a variable use (like "var(--something)") in
+# the rule view. The first argument is the variable name and the
+# second argument is the value.
+rule.variableValue=%S = %S
+
+# LOCALIZATION NOTE (rule.variableUnset): Text displayed in a tooltip
+# when the mouse is over a variable use (like "var(--something)"),
+# where the variable is not set. the rule view. The argument is the
+# variable name.
+rule.variableUnset=%S is not set
+
# LOCALIZATION NOTE (ruleView.selectorHighlighter.tooltip): Text displayed in a
# tooltip when the mouse is over a selector highlighter icon in the rule view.
rule.selectorHighlighter.tooltip=Highlight all elements matching this selector
# LOCALIZATION NOTE (rule.colorSwatch.tooltip): Text displayed in a tooltip
# when the mouse is over a color swatch in the rule view.
rule.colorSwatch.tooltip=Click to open the color picker, Shift+click to change the color format