#!/usr/bin/env python3 # Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file # for details. All rights reserved. Use of this source code is governed by a # BSD-style license that can be found in the LICENSE file. import json import os.path import re import sys _COMPAT_KEY = '__compat' _EXPERIMENTAL_KEY = 'experimental' _STATUS_KEY = 'status' _SUPPORT_KEY = 'support' _VERSION_ADDED_KEY = 'version_added' def _get_browser_compat_data(): current_dir = os.path.dirname(__file__) browser_compat_folder = os.path.abspath( os.path.join(current_dir, '..', '..', '..', 'third_party', 'mdn', 'browser-compat-data', 'src')) if not os.path.exists(browser_compat_folder): raise RuntimeError('Browser compatibility data not found at %s' % browser_compat_folder) browser_compat_data = {} INCLUDE_DIRS = [ 'api', 'html', 'svg', # TODO(srujzs): add more if needed ] # Transform to absolute paths INCLUDE_DIRS = [ os.path.join(browser_compat_folder, dir) for dir in INCLUDE_DIRS ] def process_json_dict(json_dict): # Returns a tuple of the interface name and the metadata corresponding # to it. if 'api' in json_dict: # Get the interface name api_dict = json_dict['api'] interface_name = next(iter(api_dict)) return (interface_name, api_dict[interface_name]) elif 'html' in json_dict: html_dict = json_dict['html'] if 'elements' in html_dict: elements_dict = html_dict['elements'] element_name = next(iter(elements_dict)) # Convert to WebCore name interface = str('HTML' + element_name + 'Element') return (interface, elements_dict[element_name]) elif 'svg' in json_dict: svg_dict = json_dict['svg'] if 'elements' in svg_dict: elements_dict = svg_dict['elements'] element_name = next(iter(elements_dict)) # Convert to WebCore name interface = str('SVG' + element_name + 'Element') return (interface, elements_dict[element_name]) return (None, None) # Attempts to unify two compatibility infos by taking the union of both, and # for conflicting information, taking the "stricter" of the two versions. # Updates `a` in place to represent the union of `a` and `b`. def _unify_compat(a, b): def _has_compat_data(metadata): return _COMPAT_KEY in metadata and _SUPPORT_KEY in metadata[_COMPAT_KEY] # Unifies the support statements of both metadata and updates # `support_a` in place. If either metadata do not contain simple support # statements, defaults attribute to not supported. def _unify_support(support_a, support_b): for browser in support_a.keys(): if browser in support_b: if _is_simple_support_statement(support_a[browser]) and _is_simple_support_statement(support_b[browser]): support_a[browser][_VERSION_ADDED_KEY] = _unify_versions( support_a[browser][_VERSION_ADDED_KEY], support_b[browser][_VERSION_ADDED_KEY]) else: # Only support simple statements for now. support_a[browser] = {_VERSION_ADDED_KEY: None} for browser in support_b.keys(): if not browser in support_a: support_a[browser] = support_b[browser] if not _has_compat_data(b): return if not _has_compat_data(a): a[_COMPAT_KEY] = b[_COMPAT_KEY] return support_a = a[_COMPAT_KEY][_SUPPORT_KEY] support_b = b[_COMPAT_KEY][_SUPPORT_KEY] _unify_support(support_a, support_b) # Unifies any status info in the two metadata. Modifies `a` in place to # represent the union of both `a` and `b`. def _unify_status(a, b): def _has_status(metadata): return _COMPAT_KEY in metadata and _STATUS_KEY in metadata[_COMPAT_KEY] # Modifies `status_a` in place to combine "experimental" tags. def _unify_experimental(status_a, status_b): # If either of the statuses report experimental, assume attribute is # experimental. status_a[_EXPERIMENTAL_KEY] = status_a.get( _EXPERIMENTAL_KEY, False) or status_b.get(_EXPERIMENTAL_KEY, False) if not _has_status(b): return if not _has_status(a): a[_COMPAT_KEY] = b[_COMPAT_KEY] return status_a = a[_COMPAT_KEY][_STATUS_KEY] status_b = b[_COMPAT_KEY][_STATUS_KEY] _unify_experimental(status_a, status_b) # If there exists multiple definitions of the same interface metadata e.g. # elements, this attempts to unify the compatibilities for the interface as # well as for each attribute. def _unify_metadata(a, b): # Unify the compatibility statement and status of the API or element. _unify_compat(a, b) _unify_status(a, b) # Unify the compatibility statement and status of each attribute. for attr in list(a.keys()): if attr == _COMPAT_KEY: continue if attr in b: _unify_compat(a[attr], b[attr]) _unify_status(a[attr], b[attr]) for attr in b.keys(): if not attr in a: a[attr] = b[attr] for (dir_path, dirs, files) in os.walk(browser_compat_folder): def should_process_dir(dir_path): if os.path.abspath(dir_path) == browser_compat_folder: return True for dir in INCLUDE_DIRS: if dir_path.startswith(dir): return True return False if should_process_dir(dir_path): for name in files: file_name = os.path.join(dir_path, name) (interface_path, ext) = os.path.splitext(file_name) if ext == '.json': with open(file_name) as src: json_dict = json.load(src) interface, metadata = process_json_dict(json_dict) if not interface is None: # Note: interface and member names do not # necessarily have the same capitalization as # WebCore, so we keep them all lowercase for easier # matching later. interface = interface.lower() metadata = { member.lower(): info for member, info in metadata.items() } if interface in browser_compat_data: _unify_metadata(browser_compat_data[interface], metadata) else: browser_compat_data[interface] = metadata else: dirs[:] = [] # Do not go underneath return browser_compat_data # Given two version values for a given browser, chooses the more strict version. def _unify_versions(version_a, version_b): # Given two valid version strings, compares parts of the version string # iteratively. def _greater_version(version_a, version_b): version_a_split = list(map(int, version_a.split('.'))) version_b_split = list(map(int, version_b.split('.'))) for i in range(min(len(version_a_split), len(version_b_split))): if version_a_split[i] > version_b_split[i]: return version_a elif version_a_split[i] < version_b_split[i]: return version_b return version_a if len(version_a_split) > len( version_b_split) else version_b # Validate that we can handle the given version. def _validate_version(version): if not version: return False if version is True: return True if isinstance(version, str): pattern = re.compile('^([0-9]+\.)*[0-9]+$') if not pattern.match(version): # It's possible for version strings to look like '<35'. We don't # attempt to parse the conditional logic, and just default to # potentially incompatible. return None return version else: raise ValueError( 'Type of version_a was not handled correctly! type(version) = ' + str(type(version))) version_a = _validate_version(version_a) version_b = _validate_version(version_b) # If one version reports not supported, default to not supported. if not version_a or not version_b: return False # If one version reports always supported, the other version can only be # more strict. if version_a is True: return version_b if version_b is True: return version_a return _greater_version(version_a, version_b) # At this time, we only handle simple support statements due to the complexity # and variability around support statements with multiple elements. def _is_simple_support_statement(support_statement): if isinstance(support_statement, list): # array_support_statement # TODO(srujzs): Parse this list to determine compatibility. Will # likely require parsing for 'version_removed' keys. Notes about # which browser version enabled this attribute for which # platform also complicates things. For now, we assume it's not # compatible. return False if len(support_statement.keys()) > 1: # If it's anything more complicated than 'version_added', like # 'notes' that specify platform versions, we assume it's not # compatible. return False return True class MDNReader(object): # Statically initialize and treat as constant. _BROWSER_COMPAT_DATA = _get_browser_compat_data() def __init__(self): self._compat_overrides = {} def _get_attr_compatibility(self, compat_data): # Parse schema syntax of MDN data: # https://github.com/mdn/browser-compat-data/blob/master/schemas/compat-data.schema.json # For now, we will require support for browsers since the last IDL roll. # TODO(srujzs): Determine if this is too conservative. browser_version_map = { 'chrome': '63', 'firefox': '57', 'safari': '11', # We still support the latest version of IE. 'ie': '11', 'opera': '50', } for browser in browser_version_map.keys(): support_data = compat_data[_SUPPORT_KEY] if browser not in support_data: return False support_statement = support_data[browser] if not _is_simple_support_statement(support_statement): return False version = support_statement[_VERSION_ADDED_KEY] # Compare version strings, target should be the more strict version. target = browser_version_map[browser] if _unify_versions(version, target) != target: return False # If the attribute is experimental, we assume it's not compatible. status_data = compat_data[_STATUS_KEY] if _EXPERIMENTAL_KEY in status_data and status_data[_EXPERIMENTAL_KEY]: return False return True def is_compatible(self, attribute): # Since capitalization isn't consistent across MDN and WebCore, we # compare lowercase equivalents for interface and attribute names. interface = attribute.doc_js_interface_name.lower() if interface in self._BROWSER_COMPAT_DATA and attribute.id and len( attribute.id) > 0: interface_dict = self._BROWSER_COMPAT_DATA[interface] id_name = attribute.id.lower() secure_context_key = 'isSecureContext' if interface in self._compat_overrides and id_name in self._compat_overrides[ interface]: return self._compat_overrides[interface][id_name] elif secure_context_key in interface_dict: # If the interface requires a secure context, all attributes are # implicitly incompatible. return False elif id_name in interface_dict: id_data = interface_dict[id_name] return self._get_attr_compatibility(id_data[_COMPAT_KEY]) else: # Might be an attribute that is defined in a parent interface. # We defer until attribute emitting to determine if this is the # case. Otherwise, return None. pass return None def set_compatible(self, attribute, compatible): # Override value in the MDN browser compatibility data. if not compatible in [True, False, None]: raise ValueError('Cannot set a non-boolean object for compatible') interface = attribute.doc_js_interface_name.lower() if not interface in self._compat_overrides: self._compat_overrides[interface] = {} if attribute.id and len(attribute.id) > 0: id_name = attribute.id.lower() self._compat_overrides[interface][id_name] = compatible