Flutter Engine
The Flutter Engine
Loading...
Searching...
No Matches
mdnreader.py
Go to the documentation of this file.
1#!/usr/bin/env python3
2# Copyright (c) 2020, the Dart project authors. Please see the AUTHORS file
3# for details. All rights reserved. Use of this source code is governed by a
4# BSD-style license that can be found in the LICENSE file.
5
6import json
7import os.path
8import re
9import sys
10
11_COMPAT_KEY = '__compat'
12_EXPERIMENTAL_KEY = 'experimental'
13_STATUS_KEY = 'status'
14_SUPPORT_KEY = 'support'
15_VERSION_ADDED_KEY = 'version_added'
16
17
19 current_dir = os.path.dirname(__file__)
20
21 browser_compat_folder = os.path.abspath(
22 os.path.join(current_dir, '..', '..', '..', 'third_party', 'mdn',
23 'browser-compat-data', 'src'))
24
25 if not os.path.exists(browser_compat_folder):
26 raise RuntimeError('Browser compatibility data not found at %s' %
27 browser_compat_folder)
28
29 browser_compat_data = {}
30
31 INCLUDE_DIRS = [
32 'api',
33 'html',
34 'svg',
35 # TODO(srujzs): add more if needed
36 ]
37
38 # Transform to absolute paths
39 INCLUDE_DIRS = [
40 os.path.join(browser_compat_folder, dir) for dir in INCLUDE_DIRS
41 ]
42
43 def process_json_dict(json_dict):
44 # Returns a tuple of the interface name and the metadata corresponding
45 # to it.
46 if 'api' in json_dict:
47 # Get the interface name
48 api_dict = json_dict['api']
49 interface_name = next(iter(api_dict))
50 return (interface_name, api_dict[interface_name])
51 elif 'html' in json_dict:
52 html_dict = json_dict['html']
53 if 'elements' in html_dict:
54 elements_dict = html_dict['elements']
55 element_name = next(iter(elements_dict))
56 # Convert to WebCore name
57 interface = str('HTML' + element_name + 'Element')
58 return (interface, elements_dict[element_name])
59 elif 'svg' in json_dict:
60 svg_dict = json_dict['svg']
61 if 'elements' in svg_dict:
62 elements_dict = svg_dict['elements']
63 element_name = next(iter(elements_dict))
64 # Convert to WebCore name
65 interface = str('SVG' + element_name + 'Element')
66 return (interface, elements_dict[element_name])
67 return (None, None)
68
69 # Attempts to unify two compatibility infos by taking the union of both, and
70 # for conflicting information, taking the "stricter" of the two versions.
71 # Updates `a` in place to represent the union of `a` and `b`.
72 def _unify_compat(a, b):
73
74 def _has_compat_data(metadata):
75 return _COMPAT_KEY in metadata and _SUPPORT_KEY in metadata[_COMPAT_KEY]
76
77 # Unifies the support statements of both metadata and updates
78 # `support_a` in place. If either metadata do not contain simple support
79 # statements, defaults attribute to not supported.
80 def _unify_support(support_a, support_b):
81 for browser in support_a.keys():
82 if browser in support_b:
83 if _is_simple_support_statement(support_a[browser]) and _is_simple_support_statement(support_b[browser]):
84 support_a[browser][_VERSION_ADDED_KEY] = _unify_versions(
85 support_a[browser][_VERSION_ADDED_KEY],
86 support_b[browser][_VERSION_ADDED_KEY])
87 else:
88 # Only support simple statements for now.
89 support_a[browser] = {_VERSION_ADDED_KEY: None}
90 for browser in support_b.keys():
91 if not browser in support_a:
92 support_a[browser] = support_b[browser]
93
94 if not _has_compat_data(b):
95 return
96 if not _has_compat_data(a):
97 a[_COMPAT_KEY] = b[_COMPAT_KEY]
98 return
99
100 support_a = a[_COMPAT_KEY][_SUPPORT_KEY]
101 support_b = b[_COMPAT_KEY][_SUPPORT_KEY]
102
103 _unify_support(support_a, support_b)
104
105 # Unifies any status info in the two metadata. Modifies `a` in place to
106 # represent the union of both `a` and `b`.
107 def _unify_status(a, b):
108
109 def _has_status(metadata):
110 return _COMPAT_KEY in metadata and _STATUS_KEY in metadata[_COMPAT_KEY]
111
112 # Modifies `status_a` in place to combine "experimental" tags.
113 def _unify_experimental(status_a, status_b):
114 # If either of the statuses report experimental, assume attribute is
115 # experimental.
116 status_a[_EXPERIMENTAL_KEY] = status_a.get(
117 _EXPERIMENTAL_KEY, False) or status_b.get(_EXPERIMENTAL_KEY, False)
118
119 if not _has_status(b):
120 return
121 if not _has_status(a):
122 a[_COMPAT_KEY] = b[_COMPAT_KEY]
123 return
124
125 status_a = a[_COMPAT_KEY][_STATUS_KEY]
126 status_b = b[_COMPAT_KEY][_STATUS_KEY]
127
128 _unify_experimental(status_a, status_b)
129
130 # If there exists multiple definitions of the same interface metadata e.g.
131 # elements, this attempts to unify the compatibilities for the interface as
132 # well as for each attribute.
133 def _unify_metadata(a, b):
134 # Unify the compatibility statement and status of the API or element.
135 _unify_compat(a, b)
136 _unify_status(a, b)
137 # Unify the compatibility statement and status of each attribute.
138 for attr in list(a.keys()):
139 if attr == _COMPAT_KEY:
140 continue
141 if attr in b:
142 _unify_compat(a[attr], b[attr])
143 _unify_status(a[attr], b[attr])
144 for attr in b.keys():
145 if not attr in a:
146 a[attr] = b[attr]
147
148 for (dir_path, dirs, files) in os.walk(browser_compat_folder):
149
150 def should_process_dir(dir_path):
151 if os.path.abspath(dir_path) == browser_compat_folder:
152 return True
153 for dir in INCLUDE_DIRS:
154 if dir_path.startswith(dir):
155 return True
156 return False
157
158 if should_process_dir(dir_path):
159 for name in files:
160 file_name = os.path.join(dir_path, name)
161 (interface_path, ext) = os.path.splitext(file_name)
162 if ext == '.json':
163 with open(file_name) as src:
164 json_dict = json.load(src)
165 interface, metadata = process_json_dict(json_dict)
166 if not interface is None:
167 # Note: interface and member names do not
168 # necessarily have the same capitalization as
169 # WebCore, so we keep them all lowercase for easier
170 # matching later.
171 interface = interface.lower()
172 metadata = {
173 member.lower(): info
174 for member, info in metadata.items()
175 }
176
177 if interface in browser_compat_data:
178 _unify_metadata(browser_compat_data[interface],
179 metadata)
180 else:
181 browser_compat_data[interface] = metadata
182 else:
183 dirs[:] = [] # Do not go underneath
184
185 return browser_compat_data
186
187
188# Given two version values for a given browser, chooses the more strict version.
189def _unify_versions(version_a, version_b):
190 # Given two valid version strings, compares parts of the version string
191 # iteratively.
192 def _greater_version(version_a, version_b):
193 version_a_split = list(map(int, version_a.split('.')))
194 version_b_split = list(map(int, version_b.split('.')))
195 for i in range(min(len(version_a_split), len(version_b_split))):
196 if version_a_split[i] > version_b_split[i]:
197 return version_a
198 elif version_a_split[i] < version_b_split[i]:
199 return version_b
200 return version_a if len(version_a_split) > len(
201 version_b_split) else version_b
202
203 # Validate that we can handle the given version.
204 def _validate_version(version):
205 if not version:
206 return False
207 if version is True:
208 return True
209 if isinstance(version, str):
210 pattern = re.compile('^([0-9]+\.)*[0-9]+$')
211 if not pattern.match(version):
212 # It's possible for version strings to look like '<35'. We don't
213 # attempt to parse the conditional logic, and just default to
214 # potentially incompatible.
215 return None
216 return version
217 else:
218 raise ValueError(
219 'Type of version_a was not handled correctly! type(version) = '
220 + str(type(version)))
221
222 version_a = _validate_version(version_a)
223 version_b = _validate_version(version_b)
224 # If one version reports not supported, default to not supported.
225 if not version_a or not version_b:
226 return False
227 # If one version reports always supported, the other version can only be
228 # more strict.
229 if version_a is True:
230 return version_b
231 if version_b is True:
232 return version_a
233
234 return _greater_version(version_a, version_b)
235
236
237# At this time, we only handle simple support statements due to the complexity
238# and variability around support statements with multiple elements.
239def _is_simple_support_statement(support_statement):
240 if isinstance(support_statement, list): # array_support_statement
241 # TODO(srujzs): Parse this list to determine compatibility. Will
242 # likely require parsing for 'version_removed' keys. Notes about
243 # which browser version enabled this attribute for which
244 # platform also complicates things. For now, we assume it's not
245 # compatible.
246 return False
247 if len(support_statement.keys()) > 1:
248 # If it's anything more complicated than 'version_added', like
249 # 'notes' that specify platform versions, we assume it's not
250 # compatible.
251 return False
252 return True
253
254
255class MDNReader(object):
256 # Statically initialize and treat as constant.
257 _BROWSER_COMPAT_DATA = _get_browser_compat_data()
258
259 def __init__(self):
261
262 def _get_attr_compatibility(self, compat_data):
263 # Parse schema syntax of MDN data:
264 # https://github.com/mdn/browser-compat-data/blob/master/schemas/compat-data.schema.json
265
266 # For now, we will require support for browsers since the last IDL roll.
267 # TODO(srujzs): Determine if this is too conservative.
268 browser_version_map = {
269 'chrome': '63',
270 'firefox': '57',
271 'safari': '11',
272 # We still support the latest version of IE.
273 'ie': '11',
274 'opera': '50',
275 }
276 for browser in browser_version_map.keys():
277 support_data = compat_data[_SUPPORT_KEY]
278 if browser not in support_data:
279 return False
280 support_statement = support_data[browser]
281 if not _is_simple_support_statement(support_statement):
282 return False
283 version = support_statement[_VERSION_ADDED_KEY]
284 # Compare version strings, target should be the more strict version.
285 target = browser_version_map[browser]
286 if _unify_versions(version, target) != target:
287 return False
288
289 # If the attribute is experimental, we assume it's not compatible.
290 status_data = compat_data[_STATUS_KEY]
291 if _EXPERIMENTAL_KEY in status_data and status_data[_EXPERIMENTAL_KEY]:
292 return False
293 return True
294
295 def is_compatible(self, attribute):
296 # Since capitalization isn't consistent across MDN and WebCore, we
297 # compare lowercase equivalents for interface and attribute names.
298 interface = attribute.doc_js_interface_name.lower()
299 if interface in self._BROWSER_COMPAT_DATA and attribute.id and len(
300 attribute.id) > 0:
301 interface_dict = self._BROWSER_COMPAT_DATA[interface]
302 id_name = attribute.id.lower()
303 secure_context_key = 'isSecureContext'
304 if interface in self._compat_overrides and id_name in self._compat_overrides[
305 interface]:
306 return self._compat_overrides[interface][id_name]
307 elif secure_context_key in interface_dict:
308 # If the interface requires a secure context, all attributes are
309 # implicitly incompatible.
310 return False
311 elif id_name in interface_dict:
312 id_data = interface_dict[id_name]
313 return self._get_attr_compatibility(id_data[_COMPAT_KEY])
314 else:
315 # Might be an attribute that is defined in a parent interface.
316 # We defer until attribute emitting to determine if this is the
317 # case. Otherwise, return None.
318 pass
319 return None
320
321 def set_compatible(self, attribute, compatible):
322 # Override value in the MDN browser compatibility data.
323 if not compatible in [True, False, None]:
324 raise ValueError('Cannot set a non-boolean object for compatible')
325 interface = attribute.doc_js_interface_name.lower()
326 if not interface in self._compat_overrides:
327 self._compat_overrides[interface] = {}
328 if attribute.id and len(attribute.id) > 0:
329 id_name = attribute.id.lower()
330 self._compat_overrides[interface][id_name] = compatible
static float next(float f)
is_compatible(self, attribute)
Definition mdnreader.py:295
set_compatible(self, attribute, compatible)
Definition mdnreader.py:321
_get_attr_compatibility(self, compat_data)
Definition mdnreader.py:262
static float min(float r, float g, float b)
Definition hsl.cpp:48
_unify_versions(version_a, version_b)
Definition mdnreader.py:189
_is_simple_support_statement(support_statement)
Definition mdnreader.py:239