summaryrefslogtreecommitdiffstats
path: root/devtools/shared/fronts/css-properties.js
diff options
context:
space:
mode:
Diffstat (limited to 'devtools/shared/fronts/css-properties.js')
-rw-r--r--devtools/shared/fronts/css-properties.js323
1 files changed, 323 insertions, 0 deletions
diff --git a/devtools/shared/fronts/css-properties.js b/devtools/shared/fronts/css-properties.js
new file mode 100644
index 000000000..9b3172a22
--- /dev/null
+++ b/devtools/shared/fronts/css-properties.js
@@ -0,0 +1,323 @@
+/* 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 { FrontClassWithSpec, Front } = require("devtools/shared/protocol");
+const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties");
+const { Task } = require("devtools/shared/task");
+const { CSS_PROPERTIES_DB } = require("devtools/shared/css/properties-db");
+const { cssColors } = require("devtools/shared/css/color-db");
+
+/**
+ * Build up a regular expression that matches a CSS variable token. This is an
+ * ident token that starts with two dashes "--".
+ *
+ * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
+ */
+var NON_ASCII = "[^\\x00-\\x7F]";
+var ESCAPE = "\\\\[^\n\r]";
+var FIRST_CHAR = ["[_a-z]", NON_ASCII, ESCAPE].join("|");
+var TRAILING_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|");
+var IS_VARIABLE_TOKEN = new RegExp(`^--(${FIRST_CHAR})(${TRAILING_CHAR})*$`,
+ "i");
+/**
+ * Check that this is a CSS variable.
+ *
+ * @param {String} input
+ * @return {Boolean}
+ */
+function isCssVariable(input) {
+ return !!input.match(IS_VARIABLE_TOKEN);
+}
+
+var cachedCssProperties = new WeakMap();
+
+/**
+ * The CssProperties front provides a mechanism to have a one-time asynchronous
+ * load of a CSS properties database. This is then fed into the CssProperties
+ * interface that provides synchronous methods for finding out what CSS
+ * properties the current server supports.
+ */
+const CssPropertiesFront = FrontClassWithSpec(cssPropertiesSpec, {
+ initialize: function (client, { cssPropertiesActor }) {
+ Front.prototype.initialize.call(this, client, {actor: cssPropertiesActor});
+ this.manage(this);
+ }
+});
+
+/**
+ * Ask questions to a CSS database. This class does not care how the database
+ * gets loaded in, only the questions that you can ask to it.
+ * Prototype functions are bound to 'this' so they can be passed around as helper
+ * functions.
+ *
+ * @param {Object} db
+ * A database of CSS properties
+ * @param {Object} inheritedList
+ * The key is the property name, the value is whether or not
+ * that property is inherited.
+ */
+function CssProperties(db) {
+ this.properties = db.properties;
+ this.pseudoElements = db.pseudoElements;
+
+ this.isKnown = this.isKnown.bind(this);
+ this.isInherited = this.isInherited.bind(this);
+ this.supportsType = this.supportsType.bind(this);
+ this.isValidOnClient = this.isValidOnClient.bind(this);
+
+ // A weakly held dummy HTMLDivElement to test CSS properties on the client.
+ this._dummyElements = new WeakMap();
+}
+
+CssProperties.prototype = {
+ /**
+ * Checks to see if the property is known by the browser. This function has
+ * `this` already bound so that it can be passed around by reference.
+ *
+ * @param {String} property The property name to be checked.
+ * @return {Boolean}
+ */
+ isKnown(property) {
+ return !!this.properties[property] || isCssVariable(property);
+ },
+
+ /**
+ * Quickly check if a CSS name/value combo is valid on the client.
+ *
+ * @param {String} Property name.
+ * @param {String} Property value.
+ * @param {Document} The client's document object.
+ * @return {Boolean}
+ */
+ isValidOnClient(name, value, doc) {
+ let dummyElement = this._dummyElements.get(doc);
+ if (!dummyElement) {
+ dummyElement = doc.createElement("div");
+ this._dummyElements.set(doc, dummyElement);
+ }
+
+ // `!important` is not a valid value when setting a style declaration in the
+ // CSS Object Model.
+ const sanitizedValue = ("" + value).replace(/!\s*important\s*$/, "");
+
+ // Test the style on the element.
+ dummyElement.style[name] = sanitizedValue;
+ const isValid = !!dummyElement.style[name];
+
+ // Reset the state of the dummy element;
+ dummyElement.style[name] = "";
+ return isValid;
+ },
+
+ /**
+ * Get a function that will check the validity of css name/values for a given document.
+ * Useful for injecting isValidOnClient into components when needed.
+ *
+ * @param {Document} The client's document object.
+ * @return {Function} this.isValidOnClient with the document pre-set.
+ */
+ getValidityChecker(doc) {
+ return (name, value) => this.isValidOnClient(name, value, doc);
+ },
+
+ /**
+ * 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;
+ },
+
+ /**
+ * 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.
+ * @return {Boolean}
+ */
+ supportsType(property, type) {
+ return this.properties[property] && this.properties[property].supports.includes(type);
+ },
+
+ /**
+ * Gets the CSS values for a given property name.
+ *
+ * @param {String} property The property to use.
+ * @return {Array} An array of strings.
+ */
+ getValues(property) {
+ return this.properties[property] ? this.properties[property].values : [];
+ },
+
+ /**
+ * Gets the CSS property names.
+ *
+ * @return {Array} An array of strings.
+ */
+ getNames(property) {
+ return Object.keys(this.properties);
+ },
+
+ /**
+ * Return a list of subproperties for the given property. If |name|
+ * does not name a valid property, an empty array is returned. If
+ * the property is not a shorthand property, then array containing
+ * just the property itself is returned.
+ *
+ * @param {String} name The property to query
+ * @return {Array} An array of subproperty names.
+ */
+ getSubproperties(name) {
+ if (this.isKnown(name)) {
+ if (this.properties[name] && this.properties[name].subproperties) {
+ return this.properties[name].subproperties;
+ }
+ return [name];
+ }
+ return [];
+ },
+};
+
+/**
+ * Create a CssProperties object with a fully loaded CSS database. The
+ * CssProperties interface can be queried synchronously, but the initialization
+ * is potentially async and should be handled up-front when the tool is created.
+ *
+ * The front is returned only with this function so that it can be destroyed
+ * once the toolbox is destroyed.
+ *
+ * @param {Toolbox} The current toolbox.
+ * @returns {Promise} Resolves to {cssProperties, cssPropertiesFront}.
+ */
+const initCssProperties = Task.async(function* (toolbox) {
+ const client = toolbox.target.client;
+ if (cachedCssProperties.has(client)) {
+ return cachedCssProperties.get(client);
+ }
+
+ let db, front;
+
+ // Get the list dynamically if the cssProperties actor exists.
+ if (toolbox.target.hasActor("cssProperties")) {
+ front = CssPropertiesFront(client, toolbox.target.form);
+ const serverDB = yield front.getCSSDatabase();
+
+ // Ensure the database was returned in a format that is understood.
+ // Older versions of the protocol could return a blank database.
+ if (!serverDB.properties && !serverDB.margin) {
+ db = CSS_PROPERTIES_DB;
+ } else {
+ db = serverDB;
+ }
+ } else {
+ // The target does not support this actor, so require a static list of supported
+ // properties.
+ db = CSS_PROPERTIES_DB;
+ }
+
+ const cssProperties = new CssProperties(normalizeCssData(db));
+ cachedCssProperties.set(client, {cssProperties, front});
+ return {cssProperties, front};
+});
+
+/**
+ * Synchronously get a cached and initialized CssProperties.
+ *
+ * @param {Toolbox} The current toolbox.
+ * @returns {CssProperties}
+ */
+function getCssProperties(toolbox) {
+ if (!cachedCssProperties.has(toolbox.target.client)) {
+ throw new Error("The CSS database has not been initialized, please make " +
+ "sure initCssDatabase was called once before for this " +
+ "toolbox.");
+ }
+ return cachedCssProperties.get(toolbox.target.client).cssProperties;
+}
+
+/**
+ * Get a client-side CssProperties. This is useful for dependencies in tests, or parts
+ * of the codebase that don't particularly need to match every known CSS property on
+ * the target.
+ * @return {CssProperties}
+ */
+function getClientCssProperties() {
+ return new CssProperties(normalizeCssData(CSS_PROPERTIES_DB));
+}
+
+/**
+ * Even if the target has the cssProperties actor, the returned data may not be in the
+ * same shape or have all of the data we need. This normalizes the data and fills in
+ * any missing information like color values.
+ *
+ * @return {Object} The normalized CSS database.
+ */
+function normalizeCssData(db) {
+ if (db !== CSS_PROPERTIES_DB) {
+ // Firefox 49's getCSSDatabase() just returned the properties object, but
+ // now it returns an object with multiple types of CSS information.
+ if (!db.properties) {
+ db = { properties: db };
+ }
+
+ // Fill in any missing DB information from the static database.
+ db = Object.assign({}, CSS_PROPERTIES_DB, db);
+
+ for (let name in db.properties) {
+ // Skip the current property if we can't find it in CSS_PROPERTIES_DB.
+ if (typeof CSS_PROPERTIES_DB.properties[name] !== "object") {
+ continue;
+ }
+
+ // Add "supports" information to the css properties if it's missing.
+ if (!db.properties.color.supports) {
+ db.properties[name].supports = CSS_PROPERTIES_DB.properties[name].supports;
+ }
+ // Add "values" information to the css properties if it's missing.
+ if (!db.properties.color.values) {
+ db.properties[name].values = CSS_PROPERTIES_DB.properties[name].values;
+ }
+ // Add "subproperties" information to the css properties if it's missing.
+ if (!db.properties.background.subproperties) {
+ db.properties[name].subproperties =
+ CSS_PROPERTIES_DB.properties[name].subproperties;
+ }
+ }
+ }
+
+ reattachCssColorValues(db);
+
+ return db;
+}
+
+/**
+ * Color values are omitted to save on space. Add them back here.
+ * @param {Object} The CSS database.
+ */
+function reattachCssColorValues(db) {
+ if (db.properties.color.values[0] === "COLOR") {
+ const colors = Object.keys(cssColors);
+
+ for (let name in db.properties) {
+ const property = db.properties[name];
+ // "values" can be undefined if {name} was not found in CSS_PROPERTIES_DB.
+ if (property.values && property.values[0] === "COLOR") {
+ property.values.shift();
+ property.values = property.values.concat(colors).sort();
+ }
+ }
+ }
+}
+
+module.exports = {
+ CssPropertiesFront,
+ CssProperties,
+ getCssProperties,
+ getClientCssProperties,
+ initCssProperties
+};