Skip to content

Commit

Permalink
Parse browser compatibility metadata (#144)
Browse files Browse the repository at this point in the history
parse browser-compat-data
  • Loading branch information
devoncarew committed Feb 2, 2024
1 parent 08b153c commit 393ed83
Show file tree
Hide file tree
Showing 4 changed files with 136 additions and 86 deletions.
122 changes: 122 additions & 0 deletions tool/generator/bcd.dart
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
// 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 BrowserCompatData read() {
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 api = (jsonDecode(content) as Map)['api'] as Map<String, dynamic>;
final interfaces = api.symbolNames
.map((key) => BCDInterfaceStatus(key, api[key] as Map<String, dynamic>))
.toList();
return BrowserCompatData(Map.fromIterable(
interfaces,
key: (i) => (i as BCDInterfaceStatus).name,
));
}

final Map<String, BCDInterfaceStatus> interfaces;

BrowserCompatData(this.interfaces);

BCDInterfaceStatus? retrieveInterfaceFor(String name) => interfaces[name];
}

class BCDInterfaceStatus extends BCDItem {
late final Map<String, BCDPropertyStatus> properties;

BCDInterfaceStatus(super.name, super.json) {
properties = Map.fromIterable(
json.symbolNames,
value: (name) => BCDPropertyStatus(
name as String, json[name] as Map<String, dynamic>, this),
);
}

BCDPropertyStatus? retrievePropertyFor(String name) => properties[name];
}

class BCDPropertyStatus extends BCDItem {
final BCDInterfaceStatus parent;

BCDPropertyStatus(super.name, super.json, this.parent);
}

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>;
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(', ');

int get browserCount => browsers.length;

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 coorespond to symbol names (i.e., filter out non-symbol
/// metadata (`__meta`, `__compat`, ...).
Iterable<String> get symbolNames => keys.where((key) => !key.startsWith('_'));
}
13 changes: 6 additions & 7 deletions tool/generator/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion tool/generator/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,11 @@
"author": "Dart project authors",
"license": "BSD 3",
"dependencies": {
"@mdn/browser-compat-data": "^5.5.2",
"@webref/css": "^6.10.0",
"@webref/idl": "^3.23.0"
},
"devDependencies": {
"web-specs": "^2.74.1",
"webidl2": "^24.2.2"
}
}
85 changes: 7 additions & 78 deletions tool/generator/translator.dart
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,13 @@
// 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';
import 'dart:js_interop';

import 'package:code_builder/code_builder.dart' as code;
import 'package:path/path.dart' as p;

import 'banned_names.dart';
import 'filesystem_api.dart';
import 'bcd.dart';
import 'singletons.dart';
import 'type_aliases.dart';
import 'type_union.dart';
Expand Down Expand Up @@ -486,15 +485,15 @@ class Translator {
final _includes = <idl.Includes>[];

late String _currentlyTranslatingUrl;
late WebSpecs webSpecs;
late BrowserCompatData browserCompatData;

/// Singleton so that various helper methods can access info about the AST.
static Translator? instance;

Translator(
this.packageRoot, this._librarySubDir, this._cssStyleDeclarations) {
instance = this;
webSpecs = WebSpecs.read();
browserCompatData = BrowserCompatData.read();
}

/// Set or update partial interfaces so we can have a unified interface
Expand Down Expand Up @@ -539,10 +538,6 @@ class Translator {
final libraryPath = '$_librarySubDir/${shortName.kebabToSnake}.dart';
assert(!_libraries.containsKey(libraryPath));

// TODO: Use the info from the spec to skip generation of some libraries.
// ignore: unused_local_variable
final spec = webSpecs.specFor(shortName)!;

final library = _Library(this, '$packageRoot/$libraryPath');
_libraries[libraryPath] = library;

Expand Down Expand Up @@ -839,6 +834,7 @@ class Translator {
code.ExtensionType _extensionType({
required String jsName,
required String dartClassName,
required BCDInterfaceStatus? interfaceStatus,
required List<String> implements,
required _OverridableConstructor? constructor,
required List<_OverridableOperation> operations,
Expand Down Expand Up @@ -885,6 +881,8 @@ class Translator {
// private classes, and make their first character uppercase in the process.
final dartClassName = isNamespace ? '\$${capitalize(jsName)}' : jsName;

final status = browserCompatData.retrieveInterfaceFor(name);

// We create a getter for namespaces with the expected name. We also create
// getters for a few pre-defined singleton classes.
final getterName = isNamespace ? jsName : singletons[jsName];
Expand All @@ -903,6 +901,7 @@ class Translator {
_extensionType(
jsName: jsName,
dartClassName: dartClassName,
interfaceStatus: status,
implements: implements,
constructor: interfacelike.constructor,
operations: operations,
Expand Down Expand Up @@ -965,73 +964,3 @@ class Translator {
return dartLibraries;
}
}

class WebSpecs {
static WebSpecs read() {
final path = p.join('node_modules', 'web-specs', 'index.json');
final content = (fs.readFileSync(
path.toJS,
JSReadFileOptions(encoding: 'utf8'.toJS),
) as JSString)
.toDart;
return WebSpecs(
(jsonDecode(content) as List)
.map((json) => WebSpec(json as Map<String, dynamic>))
.toList(),
);
}

final List<WebSpec> specs;

WebSpecs(this.specs);

WebSpec? specFor(String shortName) {
for (final spec in specs) {
if (spec.shortname == shortName) {
return spec;
}
}

for (final spec in specs) {
if (spec.seriesShortname == shortName) {
return spec;
}
}

return null;
}
}

class WebSpec {
final Map<String, dynamic> json;

WebSpec(this.json);

String get url => json['url'] as String;

String get shortname => json['shortname'] as String;

String? get seriesShortname {
if (!json.containsKey('series')) return null;
return (json['series'] as Map)['shortname'] as String?;
}

String get standing => json['standing'] as String;

List<String> get categories {
if (json.containsKey('categories')) {
return (json['categories'] as List).cast<String>();
} else {
return const [];
}
}

String? get releaseStatus {
if (!json.containsKey('release')) return null;
return (json['release'] as Map)['status'] as String?;
}

@override
String toString() =>
'$shortname $url $standing [${categories.join(',')}] $releaseStatus';
}

0 comments on commit 393ed83

Please sign in to comment.