tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

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;