MDNCompatibility.js (10365B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 "use strict"; 6 7 const _SUPPORT_STATE_BROWSER_NOT_FOUND = "BROWSER_NOT_FOUND"; 8 const _SUPPORT_STATE_SUPPORTED = "SUPPORTED"; 9 const _SUPPORT_STATE_UNSUPPORTED = "UNSUPPORTED"; 10 const _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED = "UNSUPPORTED_PREFIX_NEEDED"; 11 12 loader.lazyRequireGetter( 13 this, 14 "COMPATIBILITY_ISSUE_TYPE", 15 "resource://devtools/shared/constants.js", 16 true 17 ); 18 19 loader.lazyRequireGetter( 20 this, 21 ["getCompatNode", "getCompatTable"], 22 "resource://devtools/shared/compatibility/helpers.js", 23 true 24 ); 25 26 const PREFIX_REGEX = /^-\w+-/; 27 28 /** 29 * A class with methods used to query the MDN compatibility data for CSS properties and 30 * HTML nodes and attributes for specific browsers and versions. 31 */ 32 class MDNCompatibility { 33 /** 34 * Constructor. 35 * 36 * @param {JSON} cssPropertiesCompatData 37 * JSON of the compat data for CSS properties. 38 * https://github.com/mdn/browser-compat-data/tree/master/css/properties 39 */ 40 constructor(cssPropertiesCompatData) { 41 this._cssPropertiesCompatData = cssPropertiesCompatData; 42 } 43 44 /** 45 * Return the CSS related compatibility issues from given CSS declaration blocks. 46 * 47 * @param {Array} declarations 48 * CSS declarations to check. 49 * e.g. [{ name: "background-color", value: "lime" }, ...] 50 * @param {Array} browsers 51 * Restrict compatibility checks to these browsers and versions. 52 * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] 53 * @return {Array} issues 54 */ 55 getCSSDeclarationBlockIssues(declarations, browsers) { 56 const summaries = []; 57 for (const { name: property } of declarations) { 58 // Ignore CSS custom properties as any name is valid. 59 if (property.startsWith("--")) { 60 continue; 61 } 62 63 summaries.push(this._getCSSPropertyCompatSummary(browsers, property)); 64 } 65 66 // Classify to aliases summaries and normal summaries. 67 const { aliasSummaries, normalSummaries } = 68 this._classifyCSSCompatSummaries(summaries, browsers); 69 70 // Finally, convert to CSS issues. 71 return this._toCSSIssues(normalSummaries.concat(aliasSummaries)); 72 } 73 74 /** 75 * Classify the compatibility summaries that are able to get from 76 * `getCSSPropertyCompatSummary`. 77 * There are CSS properties that can specify the style with plural aliases such as 78 * `user-select`, aggregates those as the aliases summaries. 79 * 80 * @param {Array} summaries 81 * Assume the result of _getCSSPropertyCompatSummary(). 82 * @param {Array} browsers 83 * All browsers that to check 84 * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] 85 * @return Object 86 * { 87 * aliasSummaries: Array of alias summary, 88 * normalSummaries: Array of normal summary 89 * } 90 */ 91 _classifyCSSCompatSummaries(summaries, browsers) { 92 const aliasSummariesMap = new Map(); 93 const normalSummaries = summaries.filter(s => { 94 const { 95 database, 96 invalid, 97 terms, 98 unsupportedBrowsers, 99 prefixNeededBrowsers, 100 } = s; 101 102 if (invalid) { 103 return true; 104 } 105 106 const alias = this._getAlias(database, terms); 107 if (!alias) { 108 return true; 109 } 110 111 if (!aliasSummariesMap.has(alias)) { 112 aliasSummariesMap.set( 113 alias, 114 Object.assign(s, { 115 property: alias, 116 aliases: [], 117 unsupportedBrowsers: browsers, 118 prefixNeededBrowsers: browsers, 119 }) 120 ); 121 } 122 123 // Update alias summary. 124 const terminal = terms.pop(); 125 const aliasSummary = aliasSummariesMap.get(alias); 126 if (!aliasSummary.aliases.includes(terminal)) { 127 aliasSummary.aliases.push(terminal); 128 } 129 aliasSummary.unsupportedBrowsers = 130 aliasSummary.unsupportedBrowsers.filter(b => 131 unsupportedBrowsers.includes(b) 132 ); 133 aliasSummary.prefixNeededBrowsers = 134 aliasSummary.prefixNeededBrowsers.filter(b => 135 prefixNeededBrowsers.includes(b) 136 ); 137 return false; 138 }); 139 140 const aliasSummaries = [...aliasSummariesMap.values()].map(s => { 141 s.prefixNeeded = s.prefixNeededBrowsers.length !== 0; 142 return s; 143 }); 144 145 return { aliasSummaries, normalSummaries }; 146 } 147 148 _getAlias(compatNode, terms) { 149 const targetNode = getCompatNode(compatNode, terms); 150 return targetNode ? targetNode._aliasOf : null; 151 } 152 153 /** 154 * Return the compatibility summary of the terms. 155 * 156 * @param {Array} browsers 157 * All browsers that to check 158 * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] 159 * @param {Array} database 160 * MDN compatibility dataset where finds from 161 * @param {Array} terms 162 * The terms which is checked the compatibility summary from the 163 * database. The paremeters are passed as `rest parameters`. 164 * e.g. _getCompatSummary(browsers, database, "user-select", ...) 165 * @return {object} 166 * { 167 * database: The passed database as a parameter, 168 * terms: The passed terms as a parameter, 169 * url: The link which indicates the spec in MDN, 170 * deprecated: true if the spec of terms is deprecated, 171 * experimental: true if the spec of terms is experimental, 172 * unsupportedBrowsers: Array of unsupported browsers, 173 * } 174 */ 175 _getCompatSummary(browsers, database, terms) { 176 const compatTable = getCompatTable(database, terms); 177 178 if (!compatTable) { 179 return { invalid: true, unsupportedBrowsers: [] }; 180 } 181 182 const unsupportedBrowsers = []; 183 const prefixNeededBrowsers = []; 184 185 for (const browser of browsers) { 186 const state = this._getSupportState( 187 compatTable, 188 browser, 189 database, 190 terms 191 ); 192 193 switch (state) { 194 case _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED: { 195 prefixNeededBrowsers.push(browser); 196 unsupportedBrowsers.push(browser); 197 break; 198 } 199 case _SUPPORT_STATE_UNSUPPORTED: { 200 unsupportedBrowsers.push(browser); 201 break; 202 } 203 } 204 } 205 206 const { deprecated, experimental } = compatTable.status || {}; 207 208 return { 209 database, 210 terms, 211 url: compatTable.mdn_url, 212 specUrl: compatTable.spec_url, 213 deprecated, 214 experimental, 215 unsupportedBrowsers, 216 prefixNeededBrowsers, 217 }; 218 } 219 220 /** 221 * Return the compatibility summary of the CSS property. 222 * This function just adds `property` filed to the result of `_getCompatSummary`. 223 * 224 * @param {Array} browsers 225 * All browsers that to check 226 * e.g. [{ id: "firefox", name: "Firefox", version: "68" }, ...] 227 * @return {object} compatibility summary 228 */ 229 _getCSSPropertyCompatSummary(browsers, property) { 230 const summary = this._getCompatSummary( 231 browsers, 232 this._cssPropertiesCompatData, 233 [property] 234 ); 235 return Object.assign(summary, { property }); 236 } 237 238 _getSupportState(compatTable, browser, compatNode, terms) { 239 const supportList = compatTable.support[browser.id]; 240 if (!supportList) { 241 return _SUPPORT_STATE_BROWSER_NOT_FOUND; 242 } 243 244 const version = parseFloat(browser.version); 245 const terminal = terms.at(-1); 246 const prefix = terminal.match(PREFIX_REGEX)?.[0]; 247 248 let prefixNeeded = false; 249 for (const support of supportList) { 250 const { alternative_name: alternativeName, added, removed } = support; 251 252 if ( 253 // added id true when feature is supported, but we don't know the version 254 (added === true || 255 // `null` and `undefined` is when we don't know if it's supported. 256 // Since we don't want to have false negative, we consider it as supported 257 added === null || 258 added === undefined || 259 // It was added on a previous version number 260 added <= version) && 261 // `added` is false when the property isn't supported 262 added !== false && 263 // `removed` is false when the feature wasn't removevd 264 (removed === false || 265 // `null` and `undefined` is when we don't know if it was removed. 266 // Since we don't want to have false negative, we consider it as supported 267 removed === null || 268 removed === undefined || 269 // It was removed, but on a later version, so it's still supported 270 version <= removed) 271 ) { 272 if (alternativeName) { 273 if (alternativeName === terminal) { 274 return _SUPPORT_STATE_SUPPORTED; 275 } 276 } else if ( 277 support.prefix === prefix || 278 // There are compat data that are defined with prefix like "-moz-binding". 279 // In this case, we don't have to check the prefix. 280 (prefix && !this._getAlias(compatNode, terms)) 281 ) { 282 return _SUPPORT_STATE_SUPPORTED; 283 } 284 285 prefixNeeded = true; 286 } 287 } 288 289 return prefixNeeded 290 ? _SUPPORT_STATE_UNSUPPORTED_PREFIX_NEEDED 291 : _SUPPORT_STATE_UNSUPPORTED; 292 } 293 294 _hasIssue({ unsupportedBrowsers, deprecated, experimental, invalid }) { 295 // Don't apply as issue the invalid term which was not in the database. 296 return ( 297 !invalid && (unsupportedBrowsers.length || deprecated || experimental) 298 ); 299 } 300 301 _toIssue(summary, type) { 302 const issue = Object.assign({}, summary, { type }); 303 delete issue.database; 304 delete issue.terms; 305 delete issue.prefixNeededBrowsers; 306 return issue; 307 } 308 309 _toCSSIssues(summaries) { 310 const issues = []; 311 312 for (const summary of summaries) { 313 if (!this._hasIssue(summary)) { 314 continue; 315 } 316 317 const type = summary.aliases 318 ? COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY_ALIASES 319 : COMPATIBILITY_ISSUE_TYPE.CSS_PROPERTY; 320 issues.push(this._toIssue(summary, type)); 321 } 322 323 return issues; 324 } 325 } 326 327 module.exports = MDNCompatibility;