| // Copyright (c) 2024, 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 'dart:convert' hide json; |
| import 'dart:js_interop'; |
| |
| import 'package:path/path.dart' as p; |
| |
| import 'filesystem_api.dart'; |
| |
| /// A class to read from the browser-compat-data files and parse interface and |
| /// property status (standards track, experimental, deprecated) and supported |
| /// browser (chrome, safari, firefox) info. |
| class BrowserCompatData { |
| static final Map<String, Set<BCDPropertyStatus>> _eventHandlers = {}; |
| |
| /// Returns whether [name] is an event handler that is supported in any |
| /// interface. |
| static bool isEventHandlerSupported(String name) => |
| _eventHandlers[name]?.any((bcd) => bcd.shouldGenerate) == true; |
| |
| static BrowserCompatData read({required bool generateAll}) { |
| final path = |
| p.join('node_modules', '@mdn', 'browser-compat-data', 'data.json'); |
| final content = (fs.readFileSync( |
| path.toJS, |
| JSReadFileOptions(encoding: 'utf8'.toJS), |
| ) as JSString) |
| .toDart; |
| |
| final contentMap = jsonDecode(content) as Map; |
| final api = contentMap['api'] as Map<String, dynamic>; |
| // MDN files WebAssembly compat data in a separate folder, so we need to |
| // unify. |
| final webassembly = (contentMap['webassembly'] |
| as Map<String, dynamic>)['api'] as Map<String, dynamic>; |
| api.addAll(webassembly); |
| // Add info for the namespace as well. |
| api['WebAssembly'] = webassembly; |
| |
| final interfaces = <BCDInterfaceStatus>{}; |
| final globals = <String, Map<String, dynamic>>{}; |
| final globalInterfaces = <BCDInterfaceStatus>{}; |
| const globalsFilePrefix = 'api/_globals'; |
| |
| for (final symbolName in api.symbolNames) { |
| final apiInfo = api[symbolName] as Map<String, dynamic>; |
| final interface = BCDInterfaceStatus(symbolName, apiInfo, generateAll); |
| if (interface._sourceFile.startsWith(globalsFilePrefix)) { |
| // MDN stores global members e.g. `isSecureContext` in the same location |
| // as the interfaces. These are not interfaces, but rather properties |
| // that should go in `Window` and `WorkerGlobalScope`. We cache the |
| // compat data and add them directly to the relevant interfaces later. |
| // https://github.com/mdn/browser-compat-data/blob/main/docs/data-guidelines/api.md#global-apis |
| globals[symbolName] = apiInfo; |
| // The compat data for the console namespace is within this property. It |
| // should be exposed both as a global and as a namespace. |
| if (symbolName == 'console') interfaces.add(interface); |
| } else { |
| interfaces.add(interface); |
| } |
| if (symbolName == 'Window' || symbolName == 'WorkerGlobalScope') { |
| globalInterfaces.add(interface); |
| } |
| } |
| |
| globals.forEach((name, apiInfo) { |
| for (final globalInterface in globalInterfaces) { |
| globalInterface.addProperty(name, apiInfo, generateAll); |
| } |
| }); |
| |
| return BrowserCompatData( |
| Map.fromIterable( |
| interfaces, |
| key: (i) => (i as BCDInterfaceStatus).name, |
| ), |
| generateAll); |
| } |
| |
| final Map<String, BCDInterfaceStatus> interfaces; |
| |
| /// Whether to generate all the bindings regardless of property status. |
| bool generateAll = false; |
| |
| BrowserCompatData(this.interfaces, this.generateAll); |
| |
| BCDInterfaceStatus? retrieveInterfaceFor(String name) => interfaces[name]; |
| |
| bool shouldGenerateInterface(String name) => |
| generateAll || (retrieveInterfaceFor(name)?.shouldGenerate ?? false); |
| } |
| |
| class BCDInterfaceStatus extends BCDItem { |
| final Map<String, BCDPropertyStatus> _properties = {}; |
| |
| late final bool shouldGenerate; |
| |
| BCDInterfaceStatus(super.name, super.json, bool generateAll) { |
| for (final symbolName in json.symbolNames) { |
| addProperty( |
| symbolName, json[symbolName] as Map<String, dynamic>, generateAll); |
| } |
| shouldGenerate = generateAll || (standardTrack && !experimental); |
| } |
| |
| void addProperty( |
| String property, Map<String, dynamic> compat, bool generateAll) { |
| // Event compatibility data is stored as `<name_of_event>_event`. In order |
| // to have compatibility data for `onX` properties, we need to replace such |
| // property names. See https://github.com/mdn/browser-compat-data/blob/main/docs/data-guidelines/api.md#dom-events-eventname_event |
| // for more details. |
| late BCDPropertyStatus status; |
| const eventSuffix = '_event'; |
| if (property.endsWith(eventSuffix)) { |
| property = 'on${property.replaceAll(eventSuffix, '')}'; |
| status = BCDPropertyStatus(property, compat, this, generateAll); |
| BrowserCompatData._eventHandlers |
| .putIfAbsent(property, () => {}) |
| .add(status); |
| } else { |
| status = BCDPropertyStatus(property, compat, this, generateAll); |
| } |
| _properties[property] = status; |
| } |
| |
| BCDPropertyStatus? retrievePropertyFor(String name, {bool isStatic = false}) { |
| if (isStatic) name = '${name}_static'; |
| return _properties[name]; |
| } |
| } |
| |
| class BCDPropertyStatus extends BCDItem { |
| final BCDInterfaceStatus parent; |
| |
| late final bool shouldGenerate; |
| |
| BCDPropertyStatus(super.name, super.json, this.parent, bool generateAll) { |
| shouldGenerate = generateAll || (standardTrack && !experimental); |
| } |
| } |
| |
| abstract class BCDItem { |
| final String name; |
| final Map<String, dynamic> json; |
| |
| BCDItem(this.name, this.json); |
| |
| Map<String, dynamic> get _compat => json['__compat'] as Map<String, dynamic>; |
| String get _sourceFile => _compat['source_file'] as String; |
| Map<String, dynamic> get _status => _compat['status'] as Map<String, dynamic>; |
| Map<String, dynamic> get _support => |
| _compat['support'] as Map<String, dynamic>; |
| |
| bool get deprecated => _status['deprecated'] as bool? ?? false; |
| bool get experimental => _status['experimental'] as bool? ?? false; |
| bool get standardTrack => _status['standard_track'] as bool? ?? false; |
| |
| List<String> get status => [ |
| if (standardTrack) 'standards-track', |
| if (deprecated) 'deprecated', |
| if (experimental) 'experimental', |
| ]; |
| |
| String get _statusDescription => status.join(', '); |
| |
| bool get chromeSupported => _supportedInBrowser('chrome'); |
| bool get firefoxSupported => _supportedInBrowser('firefox'); |
| bool get safariSupported => _supportedInBrowser('safari'); |
| |
| List<String> get browsers => [ |
| if (chromeSupported) 'chrome', |
| if (firefoxSupported) 'firefox', |
| if (safariSupported) 'safari', |
| ]; |
| |
| String get _browsersDescription => browsers.join(', '); |
| |
| bool _supportedInBrowser(String browser) { |
| final map = (_support[browser] is List |
| ? (_support[browser] as List).first |
| : _support[browser]) as Map<String, dynamic>; |
| |
| if (map.containsKey('version_removed')) { |
| return false; |
| } |
| |
| final value = map['version_added']; |
| if (value is String) return true; |
| if (value is bool) return value; |
| return false; |
| } |
| |
| @override |
| String toString() => '$name ($_browsersDescription) [$_statusDescription]'; |
| } |
| |
| extension BCDJsonDataExtension on Map<String, dynamic> { |
| /// Return keys which correspond to symbol names (i.e., filter out non-symbol |
| /// metadata (`__meta`, `__compat`, ...). |
| Iterable<String> get symbolNames => keys.where((key) => !key.startsWith('_')); |
| } |