css-properties.js 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358
  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. "use strict";
  5. const { FrontClassWithSpec, Front } = require("devtools/shared/protocol");
  6. const { cssPropertiesSpec } = require("devtools/shared/specs/css-properties");
  7. const { Task } = require("devtools/shared/task");
  8. const { CSS_PROPERTIES_DB } = require("devtools/shared/css/properties-db");
  9. const { cssColors } = require("devtools/shared/css/color-db");
  10. /**
  11. * Build up a regular expression that matches a CSS variable token. This is an
  12. * ident token that starts with two dashes "--".
  13. *
  14. * https://www.w3.org/TR/css-syntax-3/#ident-token-diagram
  15. */
  16. var NON_ASCII = "[^\\x00-\\x7F]";
  17. var ESCAPE = "\\\\[^\n\r]";
  18. var FIRST_CHAR = ["[_a-z]", NON_ASCII, ESCAPE].join("|");
  19. var TRAILING_CHAR = ["[_a-z0-9-]", NON_ASCII, ESCAPE].join("|");
  20. var IS_VARIABLE_TOKEN = new RegExp(`^--(${FIRST_CHAR})(${TRAILING_CHAR})*$`,
  21. "i");
  22. /**
  23. * Check that this is a CSS variable.
  24. *
  25. * @param {String} input
  26. * @return {Boolean}
  27. */
  28. function isCssVariable(input) {
  29. return !!input.match(IS_VARIABLE_TOKEN);
  30. }
  31. var cachedCssProperties = new WeakMap();
  32. /**
  33. * The CssProperties front provides a mechanism to have a one-time asynchronous
  34. * load of a CSS properties database. This is then fed into the CssProperties
  35. * interface that provides synchronous methods for finding out what CSS
  36. * properties the current server supports.
  37. */
  38. const CssPropertiesFront = FrontClassWithSpec(cssPropertiesSpec, {
  39. initialize: function (client, { cssPropertiesActor }) {
  40. Front.prototype.initialize.call(this, client, {actor: cssPropertiesActor});
  41. this.manage(this);
  42. }
  43. });
  44. /**
  45. * Query the feature supporting status in the featureSet.
  46. *
  47. * @param {Hashmap} featureSet the feature set hashmap
  48. * @param {String} feature the feature name string
  49. * @return {Boolean} has the feature or not
  50. */
  51. function hasFeature(featureSet, feature) {
  52. if (feature in featureSet) {
  53. return featureSet[feature];
  54. }
  55. return false;
  56. }
  57. /**
  58. * Ask questions to a CSS database. This class does not care how the database
  59. * gets loaded in, only the questions that you can ask to it.
  60. * Prototype functions are bound to 'this' so they can be passed around as helper
  61. * functions.
  62. *
  63. * @param {Object} db
  64. * A database of CSS properties
  65. * @param {Object} inheritedList
  66. * The key is the property name, the value is whether or not
  67. * that property is inherited.
  68. */
  69. function CssProperties(db) {
  70. this.properties = db.properties;
  71. this.pseudoElements = db.pseudoElements;
  72. // supported feature
  73. this.cssColor4ColorFunction = hasFeature(db.supportedFeature,
  74. "css-color-4-color-function");
  75. this.isKnown = this.isKnown.bind(this);
  76. this.isInherited = this.isInherited.bind(this);
  77. this.supportsType = this.supportsType.bind(this);
  78. this.isValidOnClient = this.isValidOnClient.bind(this);
  79. this.supportsCssColor4ColorFunction =
  80. this.supportsCssColor4ColorFunction.bind(this);
  81. // A weakly held dummy HTMLDivElement to test CSS properties on the client.
  82. this._dummyElements = new WeakMap();
  83. }
  84. CssProperties.prototype = {
  85. /**
  86. * Checks to see if the property is known by the browser. This function has
  87. * `this` already bound so that it can be passed around by reference.
  88. *
  89. * @param {String} property The property name to be checked.
  90. * @return {Boolean}
  91. */
  92. isKnown(property) {
  93. return !!this.properties[property] || isCssVariable(property);
  94. },
  95. /**
  96. * Quickly check if a CSS name/value combo is valid on the client.
  97. *
  98. * @param {String} Property name.
  99. * @param {String} Property value.
  100. * @param {Document} The client's document object.
  101. * @return {Boolean}
  102. */
  103. isValidOnClient(name, value, doc) {
  104. let dummyElement = this._dummyElements.get(doc);
  105. if (!dummyElement) {
  106. dummyElement = doc.createElement("div");
  107. this._dummyElements.set(doc, dummyElement);
  108. }
  109. // `!important` is not a valid value when setting a style declaration in the
  110. // CSS Object Model.
  111. const sanitizedValue = ("" + value).replace(/!\s*important\s*$/, "");
  112. // Test the style on the element.
  113. dummyElement.style[name] = sanitizedValue;
  114. const isValid = !!dummyElement.style[name];
  115. // Reset the state of the dummy element;
  116. dummyElement.style[name] = "";
  117. return isValid;
  118. },
  119. /**
  120. * Get a function that will check the validity of css name/values for a given document.
  121. * Useful for injecting isValidOnClient into components when needed.
  122. *
  123. * @param {Document} The client's document object.
  124. * @return {Function} this.isValidOnClient with the document pre-set.
  125. */
  126. getValidityChecker(doc) {
  127. return (name, value) => this.isValidOnClient(name, value, doc);
  128. },
  129. /**
  130. * Checks to see if the property is an inherited one.
  131. *
  132. * @param {String} property The property name to be checked.
  133. * @return {Boolean}
  134. */
  135. isInherited(property) {
  136. return this.properties[property] && this.properties[property].isInherited;
  137. },
  138. /**
  139. * Checks if the property supports the given CSS type.
  140. * CSS types should come from devtools/shared/css/properties-db.js' CSS_TYPES.
  141. *
  142. * @param {String} property The property to be checked.
  143. * @param {Number} type One of the type values from CSS_TYPES.
  144. * @return {Boolean}
  145. */
  146. supportsType(property, type) {
  147. return this.properties[property] && this.properties[property].supports.includes(type);
  148. },
  149. /**
  150. * Gets the CSS values for a given property name.
  151. *
  152. * @param {String} property The property to use.
  153. * @return {Array} An array of strings.
  154. */
  155. getValues(property) {
  156. return this.properties[property] ? this.properties[property].values : [];
  157. },
  158. /**
  159. * Gets the CSS property names.
  160. *
  161. * @return {Array} An array of strings.
  162. */
  163. getNames(property) {
  164. return Object.keys(this.properties);
  165. },
  166. /**
  167. * Return a list of subproperties for the given property. If |name|
  168. * does not name a valid property, an empty array is returned. If
  169. * the property is not a shorthand property, then array containing
  170. * just the property itself is returned.
  171. *
  172. * @param {String} name The property to query
  173. * @return {Array} An array of subproperty names.
  174. */
  175. getSubproperties(name) {
  176. if (this.isKnown(name)) {
  177. if (this.properties[name] && this.properties[name].subproperties) {
  178. return this.properties[name].subproperties;
  179. }
  180. return [name];
  181. }
  182. return [];
  183. },
  184. /**
  185. * Checking for the css-color-4 color function support.
  186. *
  187. * @return {Boolean} Return true if the server supports css-color-4 color function.
  188. */
  189. supportsCssColor4ColorFunction() {
  190. return this.cssColor4ColorFunction;
  191. },
  192. };
  193. /**
  194. * Create a CssProperties object with a fully loaded CSS database. The
  195. * CssProperties interface can be queried synchronously, but the initialization
  196. * is potentially async and should be handled up-front when the tool is created.
  197. *
  198. * The front is returned only with this function so that it can be destroyed
  199. * once the toolbox is destroyed.
  200. *
  201. * @param {Toolbox} The current toolbox.
  202. * @returns {Promise} Resolves to {cssProperties, cssPropertiesFront}.
  203. */
  204. const initCssProperties = Task.async(function* (toolbox) {
  205. const client = toolbox.target.client;
  206. if (cachedCssProperties.has(client)) {
  207. return cachedCssProperties.get(client);
  208. }
  209. let db, front;
  210. // Get the list dynamically if the cssProperties actor exists.
  211. if (toolbox.target.hasActor("cssProperties")) {
  212. front = CssPropertiesFront(client, toolbox.target.form);
  213. const serverDB = yield front.getCSSDatabase();
  214. // Ensure the database was returned in a format that is understood.
  215. // Older versions of the protocol could return a blank database.
  216. if (!serverDB.properties && !serverDB.margin) {
  217. db = CSS_PROPERTIES_DB;
  218. } else {
  219. db = serverDB;
  220. }
  221. } else {
  222. // The target does not support this actor, so require a static list of supported
  223. // properties.
  224. db = CSS_PROPERTIES_DB;
  225. }
  226. const cssProperties = new CssProperties(normalizeCssData(db));
  227. cachedCssProperties.set(client, {cssProperties, front});
  228. return {cssProperties, front};
  229. });
  230. /**
  231. * Synchronously get a cached and initialized CssProperties.
  232. *
  233. * @param {Toolbox} The current toolbox.
  234. * @returns {CssProperties}
  235. */
  236. function getCssProperties(toolbox) {
  237. if (!cachedCssProperties.has(toolbox.target.client)) {
  238. throw new Error("The CSS database has not been initialized, please make " +
  239. "sure initCssDatabase was called once before for this " +
  240. "toolbox.");
  241. }
  242. return cachedCssProperties.get(toolbox.target.client).cssProperties;
  243. }
  244. /**
  245. * Get a client-side CssProperties. This is useful for dependencies in tests, or parts
  246. * of the codebase that don't particularly need to match every known CSS property on
  247. * the target.
  248. * @return {CssProperties}
  249. */
  250. function getClientCssProperties() {
  251. return new CssProperties(normalizeCssData(CSS_PROPERTIES_DB));
  252. }
  253. /**
  254. * Even if the target has the cssProperties actor, the returned data may not be in the
  255. * same shape or have all of the data we need. This normalizes the data and fills in
  256. * any missing information like color values.
  257. *
  258. * @return {Object} The normalized CSS database.
  259. */
  260. function normalizeCssData(db) {
  261. if (db !== CSS_PROPERTIES_DB) {
  262. // Firefox 49's getCSSDatabase() just returned the properties object, but
  263. // now it returns an object with multiple types of CSS information.
  264. if (!db.properties) {
  265. db = { properties: db };
  266. }
  267. // Fill in any missing DB information from the static database.
  268. db = Object.assign({}, CSS_PROPERTIES_DB, db);
  269. for (let name in db.properties) {
  270. // Skip the current property if we can't find it in CSS_PROPERTIES_DB.
  271. if (typeof CSS_PROPERTIES_DB.properties[name] !== "object") {
  272. continue;
  273. }
  274. // Add "supports" information to the css properties if it's missing.
  275. if (!db.properties.color.supports) {
  276. db.properties[name].supports = CSS_PROPERTIES_DB.properties[name].supports;
  277. }
  278. // Add "values" information to the css properties if it's missing.
  279. if (!db.properties.color.values) {
  280. db.properties[name].values = CSS_PROPERTIES_DB.properties[name].values;
  281. }
  282. // Add "subproperties" information to the css properties if it's missing.
  283. if (!db.properties.background.subproperties) {
  284. db.properties[name].subproperties =
  285. CSS_PROPERTIES_DB.properties[name].subproperties;
  286. }
  287. }
  288. }
  289. reattachCssColorValues(db);
  290. // If there is no supportedFeature in db, create an empty one.
  291. if (!db.supportedFeature) {
  292. db.supportedFeature = {};
  293. }
  294. return db;
  295. }
  296. /**
  297. * Color values are omitted to save on space. Add them back here.
  298. * @param {Object} The CSS database.
  299. */
  300. function reattachCssColorValues(db) {
  301. if (db.properties.color.values[0] === "COLOR") {
  302. const colors = Object.keys(cssColors);
  303. for (let name in db.properties) {
  304. const property = db.properties[name];
  305. // "values" can be undefined if {name} was not found in CSS_PROPERTIES_DB.
  306. if (property.values && property.values[0] === "COLOR") {
  307. property.values.shift();
  308. property.values = property.values.concat(colors).sort();
  309. }
  310. }
  311. }
  312. }
  313. module.exports = {
  314. CssPropertiesFront,
  315. CssProperties,
  316. getCssProperties,
  317. getClientCssProperties,
  318. initCssProperties
  319. };