|  | #!/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 |