diff --git a/.github/workflows/js_interop.yaml b/.github/workflows/js_interop.yaml index 2ea53e8c..4269e4eb 100644 --- a/.github/workflows/js_interop.yaml +++ b/.github/workflows/js_interop.yaml @@ -25,7 +25,8 @@ jobs: strategy: fail-fast: false matrix: - sdk: ['3.10', beta, dev] + # Add back in 3.12 when it's stable + sdk: [beta, dev] test_config: ['-p chrome', '-p chrome -c dart2wasm', '-p node'] steps: diff --git a/.github/workflows/js_interop_gen.yaml b/.github/workflows/js_interop_gen.yaml index 1d486215..4f218ca1 100644 --- a/.github/workflows/js_interop_gen.yaml +++ b/.github/workflows/js_interop_gen.yaml @@ -25,7 +25,8 @@ jobs: strategy: fail-fast: false matrix: - sdk: ['3.10', dev] + # Add back in 3.12 when it's stable + sdk: [beta, dev] test_config: ['', '-p chrome', '-p chrome -c dart2wasm'] steps: diff --git a/.github/workflows/web.yaml b/.github/workflows/web.yaml index e10b6933..45d85655 100644 --- a/.github/workflows/web.yaml +++ b/.github/workflows/web.yaml @@ -25,8 +25,9 @@ jobs: strategy: fail-fast: false matrix: - sdk: ['3.10', beta, dev] - test_config: ['-p chrome', '-p chrome -c dart2wasm'] + # Add back in 3.12 when it's stable + sdk: [beta, dev] + test_config: ['-p chrome -c dart2js', '-p chrome -c dart2wasm', '-p vm'] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd diff --git a/.github/workflows/web_generator.yaml b/.github/workflows/web_generator.yaml index f136b8af..53e7eade 100644 --- a/.github/workflows/web_generator.yaml +++ b/.github/workflows/web_generator.yaml @@ -27,7 +27,8 @@ jobs: strategy: fail-fast: false matrix: - sdk: ['3.10', dev] + # Add back in 3.12 when it's stable + sdk: [beta, dev] steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd @@ -51,6 +52,9 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 + with: + # Drop this when 3.12 is stable + sdk: dev - run: npm install - run: dart pub get @@ -66,6 +70,9 @@ jobs: steps: - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd - uses: dart-lang/setup-dart@65eb853c7ba17dde3be364c3d2858773e7144260 + with: + # Drop this when 3.12 is stable + sdk: dev - run: npm install - run: dart pub get diff --git a/js_interop/pubspec.yaml b/js_interop/pubspec.yaml index c6f23c8f..4dbfd866 100644 --- a/js_interop/pubspec.yaml +++ b/js_interop/pubspec.yaml @@ -4,7 +4,7 @@ description: Utility APIs for dart:js_interop and dart:js_interop_unsafe. repository: https://github.com/dart-lang/web environment: - sdk: ^3.10.0 + sdk: ^3.12.0-0 dev_dependencies: test: ^1.26.0 diff --git a/js_interop_gen/lib/src/ast/declarations.dart b/js_interop_gen/lib/src/ast/declarations.dart index d8fcf3a9..83fa2abb 100644 --- a/js_interop_gen/lib/src/ast/declarations.dart +++ b/js_interop_gen/lib/src/ast/declarations.dart @@ -76,8 +76,8 @@ sealed class TypeDeclaration extends NestableDeclaration this.constructors = const [], this.parent, this.documentation, - required ID id, - }) : _id = id; + required this._id, + }); /// [useFirstExtendeeAsRepType] is used to assert that the extension type /// generated has a representation type of the first member of [extendees] @@ -644,13 +644,13 @@ class NamespaceDeclaration extends NestableDeclaration NamespaceDeclaration({ required this.name, this.exported = true, - required ID id, + required this._id, this.dartName, this.topLevelDeclarations = const {}, this.namespaceDeclarations = const {}, this.nestableDeclarations = const {}, this.documentation, - }) : _id = id; + }); @override ExtensionType emit([covariant DeclarationOptions? options]) { diff --git a/js_interop_gen/lib/src/elements.dart b/js_interop_gen/lib/src/elements.dart index d30d88b7..d88d9799 100644 --- a/js_interop_gen/lib/src/elements.dart +++ b/js_interop_gen/lib/src/elements.dart @@ -306,6 +306,28 @@ class Parameter { } } +class IterableInfo { + final RawType? keyType; + final RawType valueType; + final bool isAsync; + + IterableInfo({this.keyType, required this.valueType, required this.isAsync}); +} + +class MaplikeInfo { + final RawType keyType; + final RawType valueType; + final bool isReadOnly; + final bool isFromIterable; + + MaplikeInfo({ + required this.keyType, + required this.valueType, + required this.isReadOnly, + required this.isFromIterable, + }); +} + class PartialInterfacelike { final String name; final String type; @@ -316,6 +338,8 @@ class PartialInterfacelike { final List extensionProperties = []; final MdnInterface? mdnInterface; OverridableConstructor? constructor; + IterableInfo? iterableInfo; + MaplikeInfo? maplikeInfo; factory PartialInterfacelike( idl.Interfacelike interfacelike, @@ -475,9 +499,43 @@ class PartialInterfacelike { ); break; case 'maplike': + final decl = member as idl.MemberDeclaration; + final types = + ((decl.idlType as JSAny) as JSArray).toDart; + if (types.length != 2) { + throw Exception('Unexpected number of type arguments for maplike'); + } + maplikeInfo = MaplikeInfo( + keyType: _getRawType(types[0]), + valueType: _getRawType(types[1]), + isReadOnly: decl.readonly, + isFromIterable: false, + ); + break; case 'setlike': + break; case 'iterable': case 'async_iterable': + final decl = member as idl.MemberDeclaration; + final types = + ((decl.idlType as JSAny) as JSArray).toDart; + RawType? keyType; + late RawType valueType; + + if (types.length == 1) { + valueType = _getRawType(types[0]); + } else if (types.length == 2) { + keyType = _getRawType(types[0]); + valueType = _getRawType(types[1]); + } else { + throw Exception('Unexpected number of type arguments for iterable'); + } + + iterableInfo = IterableInfo( + keyType: keyType, + valueType: valueType, + isAsync: decl.async, + ); break; default: throw Exception('Unrecognized member type $type'); diff --git a/js_interop_gen/lib/src/sdk_version.dart b/js_interop_gen/lib/src/sdk_version.dart index 8e86616c..e7ecbe8e 100644 --- a/js_interop_gen/lib/src/sdk_version.dart +++ b/js_interop_gen/lib/src/sdk_version.dart @@ -11,7 +11,7 @@ import 'package:yaml/yaml.dart'; /// /// For the purposes of code generation and tooling, we treat this as the /// language version of the SDK. -final dartLanguageVersion = Version(3, 10, 0); +final dartLanguageVersion = Version(3, 12, 0); /// Derives the language version from an SDK constraint. Version deriveLanguageVersion(VersionConstraint constraint) { diff --git a/js_interop_gen/lib/src/translator.dart b/js_interop_gen/lib/src/translator.dart index 710ac348..7e163018 100644 --- a/js_interop_gen/lib/src/translator.dart +++ b/js_interop_gen/lib/src/translator.dart @@ -138,10 +138,10 @@ class Translator { this._elementTagMap, { this.packageRoot, required bool generateAll, - bool generateForWeb = true, + this._generateForWeb = true, this.loadedRenameMap = const {}, required String bcdJsonPath, - }) : _generateForWeb = generateForWeb { + }) { instance = this; docProvider = DocProvider.create(); browserCompatData = BrowserCompatData.read( @@ -817,6 +817,419 @@ class Translator { for (final operation in operations) _operation(operation), ]; + List _generateMaplikeMethodsOnExtension( + String dartClassName, + MaplikeInfo info, + bool Function(String) hasOperation, + ) { + final keyInteropType = _typeReference( + info.keyType, + onlyEmitInteropTypes: true, + ); + final valueInteropType = _typeReference( + info.valueType, + onlyEmitInteropTypes: true, + ); + + return [ + if (!hasOperation('get')) + code.Method( + (b) => b + ..name = 'get' + ..annotations.addAll(_jsOverride('', alwaysEmit: true)) + ..external = true + ..returns = valueInteropType.rebuild((b) => b..isNullable = true) + ..requiredParameters.add( + code.Parameter( + (b) => b + ..name = 'key' + ..type = keyInteropType, + ), + ), + ), + if (!hasOperation('has')) + code.Method( + (b) => b + ..name = 'has' + ..annotations.addAll(_jsOverride('', alwaysEmit: true)) + ..external = true + ..returns = code.refer('bool') + ..requiredParameters.add( + code.Parameter( + (b) => b + ..name = 'key' + ..type = keyInteropType, + ), + ), + ), + if (!info.isReadOnly) ...[ + if (!hasOperation('set')) + code.Method( + (b) => b + ..name = 'set' + ..annotations.addAll(_jsOverride('', alwaysEmit: true)) + ..external = true + ..returns = code.refer('void') + ..requiredParameters.addAll([ + code.Parameter( + (b) => b + ..name = 'key' + ..type = keyInteropType, + ), + code.Parameter( + (b) => b + ..name = 'value' + ..type = valueInteropType, + ), + ]), + ), + if (!hasOperation('delete')) + code.Method( + (b) => b + ..name = 'delete' + ..annotations.addAll(_jsOverride('', alwaysEmit: true)) + ..external = true + ..returns = code.refer('bool') + ..requiredParameters.add( + code.Parameter( + (b) => b + ..name = 'key' + ..type = keyInteropType, + ), + ), + ), + if (!info.isFromIterable && !hasOperation('clear')) + code.Method( + (b) => b + ..name = 'clear' + ..annotations.addAll(_jsOverride('', alwaysEmit: true)) + ..external = true + ..returns = code.refer('void'), + ), + ], + if (!hasOperation('keys')) + code.Method( + (b) => b + ..name = 'keys' + ..annotations.addAll(_jsOverride('', alwaysEmit: true)) + ..external = true + ..returns = code.TypeReference( + (b) => b + ..symbol = 'JSIterator' + ..types.add(keyInteropType), + ), + ), + code.Method( + (b) => b + ..name = 'asMap' + ..type = code.MethodType.getter + ..returns = code.TypeReference( + (b) => b + ..symbol = 'Map' + ..types.addAll([ + _typeReference(info.keyType), + _typeReference(info.valueType), + ]), + ) + ..lambda = true + ..body = code.Code('_${dartClassName}MapView(this)'), + ), + ]; + } + + code.Class _generateMaplikeViewClass(String dartClassName, MaplikeInfo info) { + final className = '_${dartClassName}MapView'; + final rawKeyType = desugarTypedef(info.keyType) ?? info.keyType; + final keyType = _typeReference(info.keyType); + final valueType = _typeReference(info.valueType); + final keyInteropType = _typeReference( + info.keyType, + onlyEmitInteropTypes: true, + ); + final valueInteropType = _typeReference( + info.valueType, + onlyEmitInteropTypes: true, + ); + final jsObjectType = code.TypeReference((b) => b..symbol = dartClassName); + final keyCastType = _typeReference(rawKeyType).symbol; + final isInteropKeyCast = keyCastType.startsWith('JS'); + + final keyConversion = info.isFromIterable + ? 'key' + : _toJSCall('key', keyInteropType.symbol); + final valueConversion = info.isFromIterable + ? 'value' + : _toJSCall('value', valueInteropType.symbol); + var getConversion = info.isFromIterable + ? 'value' + : _toDartCall('value', valueInteropType.symbol); + if (!info.isFromIterable && + valueType.symbol == 'int' && + valueInteropType.symbol == 'JSNumber') { + getConversion = '$getConversion.toInt()'; + } + final keyToDartCall = _toDartCall('e', keyInteropType.symbol); + final keysBody = keyToDartCall == 'e' + ? 'return _jsObject.keys().toDartIterable;' + : 'return _jsObject.keys().toDartIterable.map((e) => $keyToDartCall);'; + + final keyTypeCheck = isInteropKeyCast + ? 'if (key == null || !(key as JSAny).isA<$keyCastType>()) return null;' + 'final jsKey = key as $keyCastType;' + : 'if (key is! $keyCastType) return null;'; + + final keyConversionLocal = isInteropKeyCast + ? keyConversion.replaceAll('key', 'jsKey') + : keyConversion; + + return code.Class( + (b) => b + ..name = className + ..extend = code.TypeReference( + (b) => b + ..symbol = info.isReadOnly ? 'UnmodifiableMapBase' : 'MapBase' + ..url = 'dart:collection' + ..types.addAll([keyType, valueType]), + ) + ..fields.add( + code.Field( + (b) => b + ..name = '_jsObject' + ..type = jsObjectType + ..modifier = code.FieldModifier.final$, + ), + ) + ..constructors.add( + code.Constructor( + (b) => b + ..requiredParameters.add( + code.Parameter( + (b) => b + ..name = '_jsObject' + ..toThis = true, + ), + ), + ), + ) + ..methods.addAll([ + code.Method( + (b) => b + ..name = 'operator []' + ..annotations.add(code.refer('override')) + ..returns = valueType.rebuild((b) => b..isNullable = true) + ..requiredParameters.add( + code.Parameter( + (b) => b + ..name = 'key' + ..type = code.refer('Object?'), + ), + ) + ..body = code.Code( + valueType.symbol == 'JSArray' + ? ''' +$keyTypeCheck +final value = _jsObject.get($keyConversionLocal); +if (value == null) return null; +return _jsObject.getAll($keyConversionLocal); +''' + : ''' +$keyTypeCheck +final value = _jsObject.get($keyConversionLocal); +if (value == null) return null; +return $getConversion; +''', + ), + ), + if (!info.isReadOnly) ...[ + code.Method( + (b) => b + ..name = 'operator []=' + ..annotations.add(code.refer('override')) + ..returns = code.refer('void') + ..requiredParameters.addAll([ + code.Parameter( + (b) => b + ..name = 'key' + ..type = keyType, + ), + code.Parameter( + (b) => b + ..name = 'value' + ..type = valueType, + ), + ]) + ..body = code.Code( + '_jsObject.set($keyConversion, $valueConversion);', + ), + ), + code.Method( + (b) => b + ..name = 'clear' + ..annotations.add(code.refer('override')) + ..returns = code.refer('void') + ..body = info.isFromIterable + ? code.Code(''' +final keys = _jsObject.keys().toDartIterable.toList(); +for (final k in keys) { + _jsObject.delete(${_toDartCall('k', keyInteropType.symbol)}); +} +''') + : const code.Code('_jsObject.clear();'), + ), + ], + code.Method( + (b) => b + ..name = 'keys' + ..type = code.MethodType.getter + ..annotations.add(code.refer('override')) + ..returns = code.TypeReference( + (b) => b + ..symbol = 'Iterable' + ..types.add(keyType), + ) + ..body = code.Code(keysBody), + ), + if (!info.isReadOnly) + code.Method( + (b) => b + ..name = 'remove' + ..annotations.add(code.refer('override')) + ..returns = valueType.rebuild((b) => b..isNullable = true) + ..requiredParameters.add( + code.Parameter( + (b) => b + ..name = 'key' + ..type = code.refer('Object?'), + ), + ) + ..body = code.Code( + valueType.symbol == 'JSArray' + ? ''' +$keyTypeCheck +final values = _jsObject.getAll($keyConversionLocal); +// ignore: prefer_is_empty +if (values.length == 0) return null; +_jsObject.delete($keyConversionLocal); +return values; +''' + : ''' +$keyTypeCheck +final value = _jsObject.get($keyConversionLocal); +_jsObject.delete($keyConversionLocal); +if (value == null) return null; +return $getConversion; +''', + ), + ), + ]), + ); + } + + code.Method _generateToDartGetter(IterableInfo info) { + final isKeyValue = info.keyType != null; + + if (isKeyValue) { + final keyInteropType = _typeReference( + info.keyType!, + onlyEmitInteropTypes: true, + ); + final valueInteropType = _typeReference( + info.valueType, + onlyEmitInteropTypes: true, + ); + + final keyDartSymbol = _dartTypeSymbol(keyInteropType.symbol); + final valueDartSymbol = _dartTypeSymbol(valueInteropType.symbol); + + final rawKeyType = desugarTypedef(info.keyType!) ?? info.keyType!; + final rawValueType = desugarTypedef(info.valueType) ?? info.valueType; + + final keyConversion = _toDartCall( + rawKeyType.type == 'JSAny' + ? 'e.toDart[0]' + : '(e.toDart[0] as ${keyInteropType.symbol})', + keyInteropType.symbol, + ); + final valueConversion = _toDartCall( + rawValueType.type == 'JSAny' + ? 'e.toDart[1]' + : '(e.toDart[1] as ${valueInteropType.symbol})', + valueInteropType.symbol, + ); + + return code.Method( + (b) => b + ..name = 'toDart' + ..type = code.MethodType.getter + ..lambda = true + ..returns = code.TypeReference( + (b) => b + ..symbol = 'Iterable' + ..types.add( + code.TypeReference( + (b) => b + ..symbol = '({$keyDartSymbol key, $valueDartSymbol value})', + ), + ), + ) + ..body = code.Code(''' +toDartIterable.map((e) => ( + key: $keyConversion, + value: $valueConversion, + ))'''), + ); + } else { + final valueInteropType = _typeReference( + info.valueType, + onlyEmitInteropTypes: true, + ); + final valueDartSymbol = _dartTypeSymbol(valueInteropType.symbol); + + final body = valueInteropType.symbol != valueDartSymbol + ? 'toDartIterable' + '.map((e) => ${_toDartCall('e', valueInteropType.symbol)})' + : 'toDartIterable'; + + return code.Method( + (b) => b + ..name = 'toDart' + ..type = code.MethodType.getter + ..lambda = true + ..returns = code.TypeReference( + (b) => b + ..symbol = 'Iterable' + ..types.add( + code.TypeReference((b) => b..symbol = valueDartSymbol), + ), + ) + ..body = code.Code(body), + ); + } + } + + String _toJSCall(String sourceExpr, String interopSymbol) => + switch (interopSymbol) { + 'JSNumber' => '$sourceExpr.toJS', + 'JSString' => '$sourceExpr.toJS', + 'JSBoolean' => '$sourceExpr.toJS', + _ => sourceExpr, + }; + + String _toDartCall(String sourceExpr, String interopSymbol) => + switch (interopSymbol) { + 'JSNumber' => '$sourceExpr.toDartDouble', + 'JSString' => '$sourceExpr.toDart', + 'JSBoolean' => '$sourceExpr.toDart', + _ => sourceExpr, + }; + + String _dartTypeSymbol(String interopSymbol) => switch (interopSymbol) { + 'JSNumber' => 'double', + 'JSString' => 'String', + 'JSBoolean' => 'bool', + _ => interopSymbol, + }; + List _cssStyleDeclarationProperties() { return [ for (final style in _cssStyleDeclarations) @@ -915,11 +1328,26 @@ class Translator { required List staticOperations, required List properties, required bool isObjectLiteral, + IterableInfo? iterableInfo, + MaplikeInfo? maplikeInfo, }) { final docs = mdnInterface == null ? [] : mdnInterface.formattedDocs; final jsObject = _typeReference(RawType('JSObject', false)); const representationFieldName = '_'; + bool hasOperation(String name) { + var c = _interfacelikes[jsName]; + while (c != null) { + if (c.operations.containsKey(name)) return true; + if (c.inheritance != null) { + c = _interfacelikes[c.inheritance!]; + } else { + break; + } + } + return false; + } + final legacyNameSpace = extendedAttributes .where( (extendedAttribute) => extendedAttribute.name == 'LegacyNamespace', @@ -955,7 +1383,37 @@ class Translator { ..implements.addAll( implements .map((interface) => _typeReference(RawType(interface, false))) - .followedBy([jsObject]), + .followedBy([jsObject]) + .followedBy( + iterableInfo != null && !iterableInfo.isAsync + ? [ + code.TypeReference( + (b) => b + ..symbol = 'JSIterable' + ..url = 'dart:js_interop' + ..types.add( + iterableInfo.keyType != null + ? code.TypeReference( + (b) => b + ..symbol = 'JSArray' + ..url = 'dart:js_interop' + ..types.add( + code.TypeReference( + (b) => b + ..symbol = 'JSAny' + ..url = 'dart:js_interop', + ), + ), + ) + : _typeReference( + iterableInfo.valueType, + onlyEmitInteropTypes: true, + ), + ), + ), + ] + : [], + ), ) ..constructors.addAll( (isObjectLiteral @@ -981,6 +1439,20 @@ class Translator { dartClassName == 'CSSStyleDeclaration' ? _cssStyleDeclarationProperties() : [], + ) + .followedBy( + iterableInfo != null && !iterableInfo.isAsync + ? [_generateToDartGetter(iterableInfo)] + : [], + ) + .followedBy( + maplikeInfo != null + ? _generateMaplikeMethodsOnExtension( + dartClassName, + maplikeInfo, + hasOperation, + ) + : [], ), ), ); @@ -1020,6 +1492,50 @@ class Translator { _renamedClasses[jsName] = dartClassName; } + var maplikeInfo = interfacelike.maplikeInfo; + + // Lookup iterableInfo from superclasses if not present. + var iterableInfo = interfacelike.iterableInfo; + var current = interfacelike; + while (iterableInfo == null && current.inheritance != null) { + final superInterface = _interfacelikes[current.inheritance!]; + if (superInterface == null) break; + iterableInfo = superInterface.iterableInfo; + current = superInterface; + } + + if (maplikeInfo == null && + iterableInfo != null && + iterableInfo.keyType != null) { + bool hasOperation(String name) { + var c = interfacelike; + while (true) { + if (c.operations.containsKey(name)) return true; + if (c.inheritance != null) { + final superInterface = _interfacelikes[c.inheritance!]; + if (superInterface == null) break; + c = superInterface; + } else { + break; + } + } + return false; + } + + final hasGet = hasOperation('get'); + final hasSet = hasOperation('set'); + final hasHas = hasOperation('has'); + + if (hasGet && hasHas) { + maplikeInfo = MaplikeInfo( + keyType: iterableInfo.keyType!, + valueType: iterableInfo.valueType, + isReadOnly: !hasSet, + isFromIterable: true, + ); + } + } + return [ if (getterName != null) _topLevelGetter(rawType, getterName), _extensionType( @@ -1034,7 +1550,11 @@ class Translator { staticOperations: staticOperations, properties: properties, isObjectLiteral: isDictionary, + iterableInfo: interfacelike.iterableInfo, + maplikeInfo: maplikeInfo, ), + if (maplikeInfo != null) + _generateMaplikeViewClass(dartClassName, maplikeInfo), if (extensionProperties.isNotEmpty) _extension(type: rawType, extensionProperties: extensionProperties), ]; diff --git a/js_interop_gen/pubspec.yaml b/js_interop_gen/pubspec.yaml index 3863bbc8..e780baf4 100644 --- a/js_interop_gen/pubspec.yaml +++ b/js_interop_gen/pubspec.yaml @@ -6,7 +6,7 @@ description: >- repository: https://github.com/dart-lang/web environment: - sdk: ^3.10.0 + sdk: ^3.12.0-0 dependencies: analyzer: ^12.0.0 diff --git a/js_interop_gen/test/integration/idl/iterable_expected.dart b/js_interop_gen/test/integration/idl/iterable_expected.dart new file mode 100644 index 00000000..13dc6529 --- /dev/null +++ b/js_interop_gen/test/integration/idl/iterable_expected.dart @@ -0,0 +1,23 @@ +// Generated from Web IDL definitions. + +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + +@JS() +library; + +import 'dart:js_interop'; + +extension type ValueIterable._(JSObject _) + implements JSObject, JSIterable { + Iterable get toDart => toDartIterable.map((e) => e.toDartDouble); +} +extension type KeyValueIterable._(JSObject _) + implements JSObject, JSIterable> { + Iterable<({String key, String value})> get toDart => toDartIterable.map( + (e) => ( + key: (e.toDart[0] as JSString).toDart, + value: (e.toDart[1] as JSString).toDart, + ), + ); +} +extension type AsyncIterable._(JSObject _) implements JSObject {} diff --git a/js_interop_gen/test/integration/idl/iterable_input.idl b/js_interop_gen/test/integration/idl/iterable_input.idl new file mode 100644 index 00000000..d00837fc --- /dev/null +++ b/js_interop_gen/test/integration/idl/iterable_input.idl @@ -0,0 +1,14 @@ +[Exposed=Window] +interface ValueIterable { + iterable; +}; + +[Exposed=Window] +interface KeyValueIterable { + iterable; +}; + +[Exposed=Window] +interface AsyncIterable { + async iterable; +}; diff --git a/js_interop_gen/test/integration/idl/maplike_expected.dart b/js_interop_gen/test/integration/idl/maplike_expected.dart new file mode 100644 index 00000000..85bfc300 --- /dev/null +++ b/js_interop_gen/test/integration/idl/maplike_expected.dart @@ -0,0 +1,92 @@ +// Generated from Web IDL definitions. + +// ignore_for_file: constant_identifier_names, non_constant_identifier_names + +@JS() +library; + +import 'dart:collection'; +import 'dart:js_interop'; + +extension type MyMaplike._(JSObject _) implements JSObject { + @JS() + external JSString? get(JSString key); + @JS() + external bool has(JSString key); + @JS() + external JSIterator keys(); + Map get asMap => _MyMaplikeMapView(this); +} + +class _MyMaplikeMapView extends UnmodifiableMapBase { + _MyMaplikeMapView(this._jsObject); + + final MyMaplike _jsObject; + + @override + String? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + if (value == null) return null; + return value.toDart; + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } +} + +extension type MyReadWriteMaplike._(JSObject _) implements JSObject { + @JS() + external JSString? get(JSString key); + @JS() + external bool has(JSString key); + @JS() + external void set(JSString key, JSString value); + @JS() + external bool delete(JSString key); + @JS() + external void clear(); + @JS() + external JSIterator keys(); + Map get asMap => _MyReadWriteMaplikeMapView(this); +} + +class _MyReadWriteMaplikeMapView extends MapBase { + _MyReadWriteMaplikeMapView(this._jsObject); + + final MyReadWriteMaplike _jsObject; + + @override + String? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + if (value == null) return null; + return value.toDart; + } + + @override + void operator []=(String key, String value) { + _jsObject.set(key.toJS, value.toJS); + } + + @override + void clear() { + _jsObject.clear(); + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } + + @override + String? remove(Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + _jsObject.delete(key.toJS); + if (value == null) return null; + return value.toDart; + } +} diff --git a/js_interop_gen/test/integration/idl/maplike_input.idl b/js_interop_gen/test/integration/idl/maplike_input.idl new file mode 100644 index 00000000..661fd423 --- /dev/null +++ b/js_interop_gen/test/integration/idl/maplike_input.idl @@ -0,0 +1,9 @@ +[Exposed=Window] +interface MyMaplike { + readonly maplike; +}; + +[Exposed=Window] +interface MyReadWriteMaplike { + maplike; +}; diff --git a/web/lib/src/dom/css_highlight_api.dart b/web/lib/src/dom/css_highlight_api.dart index d5ac85d7..49146178 100644 --- a/web/lib/src/dom/css_highlight_api.dart +++ b/web/lib/src/dom/css_highlight_api.dart @@ -13,6 +13,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'dom.dart'; @@ -96,4 +97,56 @@ extension type Highlight._(JSObject _) implements JSObject { /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/HighlightRegistry). -extension type HighlightRegistry._(JSObject _) implements JSObject {} +extension type HighlightRegistry._(JSObject _) implements JSObject { + @JS() + external Highlight? get(JSString key); + @JS() + external bool has(JSString key); + @JS() + external void set(JSString key, Highlight value); + @JS() + external bool delete(JSString key); + @JS() + external void clear(); + @JS() + external JSIterator keys(); + Map get asMap => _HighlightRegistryMapView(this); +} + +class _HighlightRegistryMapView extends MapBase { + _HighlightRegistryMapView(this._jsObject); + + final HighlightRegistry _jsObject; + + @override + Highlight? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + if (value == null) return null; + return value; + } + + @override + void operator []=(String key, Highlight value) { + _jsObject.set(key.toJS, value); + } + + @override + void clear() { + _jsObject.clear(); + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } + + @override + Highlight? remove(Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + _jsObject.delete(key.toJS); + if (value == null) return null; + return value; + } +} diff --git a/web/lib/src/dom/css_typed_om.dart b/web/lib/src/dom/css_typed_om.dart index 8cbbeba5..2d93de90 100644 --- a/web/lib/src/dom/css_typed_om.dart +++ b/web/lib/src/dom/css_typed_om.dart @@ -13,6 +13,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'geometry.dart'; @@ -68,7 +69,8 @@ extension type CSSStyleValue._(JSObject _) implements JSObject { /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/StylePropertyMapReadOnly). -extension type StylePropertyMapReadOnly._(JSObject _) implements JSObject { +extension type StylePropertyMapReadOnly._(JSObject _) + implements JSObject, JSIterable> { /// The **`get()`** method of the /// [StylePropertyMapReadOnly] interface returns a [CSSStyleValue] /// object for the first value of the specified property. @@ -88,6 +90,37 @@ extension type StylePropertyMapReadOnly._(JSObject _) implements JSObject { /// [StylePropertyMapReadOnly] interface returns an unsigned long integer /// containing the size of the `StylePropertyMapReadOnly` object. external int get size; + Iterable<({String key, JSArray value})> get toDart => toDartIterable.map( + (e) => ( + key: (e.toDart[0] as JSString).toDart, + value: (e.toDart[1] as JSArray), + ), + ); + + @JS() + external JSIterator keys(); + Map> get asMap => + _StylePropertyMapReadOnlyMapView(this); +} + +class _StylePropertyMapReadOnlyMapView + extends UnmodifiableMapBase> { + _StylePropertyMapReadOnlyMapView(this._jsObject); + + final StylePropertyMapReadOnly _jsObject; + + @override + JSArray? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key); + if (value == null) return null; + return _jsObject.getAll(key); + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } } /// The **`StylePropertyMap`** interface of the @@ -130,6 +163,52 @@ extension type StylePropertyMap._(JSObject _) /// The **`clear()`** method of the [StylePropertyMap] /// interface removes all declarations in the `StylePropertyMap`. external void clear(); + @JS() + external JSIterator keys(); + Map> get asMap => + _StylePropertyMapMapView(this); +} + +class _StylePropertyMapMapView extends MapBase> { + _StylePropertyMapMapView(this._jsObject); + + final StylePropertyMap _jsObject; + + @override + JSArray? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key); + if (value == null) return null; + return _jsObject.getAll(key); + } + + @override + void operator []=(String key, JSArray value) { + _jsObject.set(key, value); + } + + @override + void clear() { + final keys = _jsObject.keys().toDartIterable.toList(); + for (final k in keys) { + _jsObject.delete(k.toDart); + } + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } + + @override + JSArray? remove(Object? key) { + if (key is! String) return null; + final values = _jsObject.getAll(key); + // ignore: prefer_is_empty + if (values.length == 0) return null; + _jsObject.delete(key); + return values; + } } /// The **`CSSUnparsedValue`** interface of the @@ -146,7 +225,7 @@ extension type StylePropertyMap._(JSObject _) /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/CSSUnparsedValue). extension type CSSUnparsedValue._(JSObject _) - implements CSSStyleValue, JSObject { + implements CSSStyleValue, JSObject, JSIterable { external factory CSSUnparsedValue(JSArray members); external CSSUnparsedSegment operator [](int index); @@ -155,6 +234,7 @@ extension type CSSUnparsedValue._(JSObject _) /// The **`length`** read-only property of the /// [CSSUnparsedValue] interface returns the number of items in the object. external int get length; + Iterable get toDart => toDartIterable; } /// The **`CSSVariableReferenceValue`** interface of the @@ -517,13 +597,15 @@ extension type CSSMathClamp._(JSObject _) implements CSSMathValue, JSObject { /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/CSSNumericArray). -extension type CSSNumericArray._(JSObject _) implements JSObject { +extension type CSSNumericArray._(JSObject _) + implements JSObject, JSIterable { external CSSNumericValue operator [](int index); /// The read-only **`length`** property of the /// [CSSNumericArray] interface returns the number of /// [CSSNumericValue] objects in the list. external int get length; + Iterable get toDart => toDartIterable; } /// The **`CSSTransformValue`** interface of the @@ -535,7 +617,7 @@ extension type CSSNumericArray._(JSObject _) implements JSObject { /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/CSSTransformValue). extension type CSSTransformValue._(JSObject _) - implements CSSStyleValue, JSObject { + implements CSSStyleValue, JSObject, JSIterable { external factory CSSTransformValue(JSArray transforms); external CSSTransformComponent operator [](int index); @@ -559,6 +641,7 @@ extension type CSSTransformValue._(JSObject _) /// which /// case it returns false. external bool get is2D; + Iterable get toDart => toDartIterable; } /// The **`CSSTransformComponent`** interface of the diff --git a/web/lib/src/dom/dom.dart b/web/lib/src/dom/dom.dart index 43959fa6..b17e97ae 100644 --- a/web/lib/src/dom/dom.dart +++ b/web/lib/src/dom/dom.dart @@ -671,7 +671,7 @@ extension type AbortSignal._(JSObject _) implements EventTarget, JSObject { /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/NodeList). -extension type NodeList._(JSObject _) implements JSObject { +extension type NodeList._(JSObject _) implements JSObject, JSIterable { /// Returns a node from a /// [`NodeList`](https://developer.mozilla.org/en-US/docs/Web/API/NodeList) by /// index. This method @@ -688,6 +688,7 @@ extension type NodeList._(JSObject _) implements JSObject { /// The **`NodeList.length`** property returns the number of items /// in a [NodeList]. external int get length; + Iterable get toDart => toDartIterable; } /// The **`HTMLCollection`** interface represents a generic collection @@ -5379,7 +5380,8 @@ extension type TreeWalker._(JSObject _) implements JSObject { /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/DOMTokenList). -extension type DOMTokenList._(JSObject _) implements JSObject { +extension type DOMTokenList._(JSObject _) + implements JSObject, JSIterable { /// The **`item()`** method of the [DOMTokenList] interface returns an item in /// the list, /// determined by its position in the list, its index. @@ -5441,6 +5443,7 @@ extension type DOMTokenList._(JSObject _) implements JSObject { /// string, or clears and sets the list to the given value. external String get value; external set value(String value); + Iterable get toDart => toDartIterable.map((e) => e.toDart); } /// The **`XPathResult`** interface represents the results generated by diff --git a/web/lib/src/dom/encrypted_media.dart b/web/lib/src/dom/encrypted_media.dart index edd50be8..17ab3eec 100644 --- a/web/lib/src/dom/encrypted_media.dart +++ b/web/lib/src/dom/encrypted_media.dart @@ -13,6 +13,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'dom.dart'; @@ -234,7 +235,8 @@ extension type MediaKeySession._(JSObject _) implements EventTarget, JSObject { /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/MediaKeyStatusMap). -extension type MediaKeyStatusMap._(JSObject _) implements JSObject { +extension type MediaKeyStatusMap._(JSObject _) + implements JSObject, JSIterable> { /// The **`has()`** method of the /// [MediaKeyStatusMap] interface returns a `Boolean`, asserting /// whether a value has been associated with the given key. @@ -252,6 +254,38 @@ extension type MediaKeyStatusMap._(JSObject _) implements JSObject { /// the [MediaKeyStatusMap] interface returns the number of key/value paIrs /// in the status map. external int get size; + Iterable<({BufferSource key, String value})> get toDart => toDartIterable.map( + (e) => ( + key: (e.toDart[0] as BufferSource), + value: (e.toDart[1] as JSString).toDart, + ), + ); + + @JS() + external JSIterator keys(); + Map get asMap => + _MediaKeyStatusMapMapView(this); +} + +class _MediaKeyStatusMapMapView + extends UnmodifiableMapBase { + _MediaKeyStatusMapMapView(this._jsObject); + + final MediaKeyStatusMap _jsObject; + + @override + MediaKeyStatus? operator [](Object? key) { + if (key == null || !(key as JSAny).isA()) return null; + final jsKey = key as JSObject; + final value = _jsObject.get(jsKey); + if (value == null) return null; + return value; + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable; + } } /// The **`MediaKeyMessageEvent`** interface of the diff --git a/web/lib/src/dom/event_timing.dart b/web/lib/src/dom/event_timing.dart index e99042e7..64dac03e 100644 --- a/web/lib/src/dom/event_timing.dart +++ b/web/lib/src/dom/event_timing.dart @@ -13,6 +13,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'dom.dart'; @@ -69,4 +70,31 @@ extension type PerformanceEventTiming._(JSObject _) /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/EventCounts). -extension type EventCounts._(JSObject _) implements JSObject {} +extension type EventCounts._(JSObject _) implements JSObject { + @JS() + external JSNumber? get(JSString key); + @JS() + external bool has(JSString key); + @JS() + external JSIterator keys(); + Map get asMap => _EventCountsMapView(this); +} + +class _EventCountsMapView extends UnmodifiableMapBase { + _EventCountsMapView(this._jsObject); + + final EventCounts _jsObject; + + @override + int? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + if (value == null) return null; + return value.toDartDouble.toInt(); + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } +} diff --git a/web/lib/src/dom/fetch.dart b/web/lib/src/dom/fetch.dart index 9a3c612b..65366bab 100644 --- a/web/lib/src/dom/fetch.dart +++ b/web/lib/src/dom/fetch.dart @@ -15,6 +15,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'attribution_reporting_api.dart'; @@ -73,7 +74,8 @@ typedef ResponseType = String; /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/Headers). -extension type Headers._(JSObject _) implements JSObject { +extension type Headers._(JSObject _) + implements JSObject, JSIterable> { external factory Headers([HeadersInit init]); /// The **`append()`** method of the [Headers] @@ -150,6 +152,57 @@ extension type Headers._(JSObject _) implements JSObject { /// headers include the /// and . external void set(String name, String value); + Iterable<({String key, String value})> get toDart => toDartIterable.map( + (e) => ( + key: (e.toDart[0] as JSString).toDart, + value: (e.toDart[1] as JSString).toDart, + ), + ); + + @JS() + external JSIterator keys(); + Map get asMap => _HeadersMapView(this); +} + +class _HeadersMapView extends MapBase { + _HeadersMapView(this._jsObject); + + final Headers _jsObject; + + @override + String? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key); + if (value == null) return null; + return value; + } + + @override + void operator []=(String key, String value) { + _jsObject.set(key, value); + } + + @override + void clear() { + final keys = _jsObject.keys().toDartIterable.toList(); + for (final k in keys) { + _jsObject.delete(k.toDart); + } + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } + + @override + String? remove(Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key); + _jsObject.delete(key); + if (value == null) return null; + return value; + } } /// The **`Request`** interface of the diff --git a/web/lib/src/dom/url.dart b/web/lib/src/dom/url.dart index 960ae176..740f2071 100644 --- a/web/lib/src/dom/url.dart +++ b/web/lib/src/dom/url.dart @@ -13,6 +13,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; /// The **`URL`** interface is used to parse, construct, normalize, and encode . @@ -286,7 +287,8 @@ extension type URL._(JSObject _) implements JSObject { /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/URLSearchParams). -extension type URLSearchParams._(JSObject _) implements JSObject { +extension type URLSearchParams._(JSObject _) + implements JSObject, JSIterable> { external factory URLSearchParams([JSAny init]); /// The **`append()`** method of the [URLSearchParams] @@ -350,4 +352,55 @@ extension type URLSearchParams._(JSObject _) implements JSObject { /// The **`size`** read-only property of the [URLSearchParams] interface /// indicates the total number of search parameter entries. external int get size; + Iterable<({String key, String value})> get toDart => toDartIterable.map( + (e) => ( + key: (e.toDart[0] as JSString).toDart, + value: (e.toDart[1] as JSString).toDart, + ), + ); + + @JS() + external JSIterator keys(); + Map get asMap => _URLSearchParamsMapView(this); +} + +class _URLSearchParamsMapView extends MapBase { + _URLSearchParamsMapView(this._jsObject); + + final URLSearchParams _jsObject; + + @override + String? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key); + if (value == null) return null; + return value; + } + + @override + void operator []=(String key, String value) { + _jsObject.set(key, value); + } + + @override + void clear() { + final keys = _jsObject.keys().toDartIterable.toList(); + for (final k in keys) { + _jsObject.delete(k.toDart); + } + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } + + @override + String? remove(Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key); + _jsObject.delete(key); + if (value == null) return null; + return value; + } } diff --git a/web/lib/src/dom/webaudio.dart b/web/lib/src/dom/webaudio.dart index d1a1c49f..97286ea4 100644 --- a/web/lib/src/dom/webaudio.dart +++ b/web/lib/src/dom/webaudio.dart @@ -13,6 +13,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'dom.dart'; @@ -3496,7 +3497,34 @@ extension type AudioWorkletGlobalScope._(JSObject _) /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/AudioParamMap). -extension type AudioParamMap._(JSObject _) implements JSObject {} +extension type AudioParamMap._(JSObject _) implements JSObject { + @JS() + external AudioParam? get(JSString key); + @JS() + external bool has(JSString key); + @JS() + external JSIterator keys(); + Map get asMap => _AudioParamMapMapView(this); +} + +class _AudioParamMapMapView extends UnmodifiableMapBase { + _AudioParamMapMapView(this._jsObject); + + final AudioParamMap _jsObject; + + @override + AudioParam? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + if (value == null) return null; + return value; + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } +} /// > [!NOTE] /// > Although the interface is available outside diff --git a/web/lib/src/dom/webmidi.dart b/web/lib/src/dom/webmidi.dart index ee054cc4..aa7fb9b5 100644 --- a/web/lib/src/dom/webmidi.dart +++ b/web/lib/src/dom/webmidi.dart @@ -13,6 +13,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'dom.dart'; @@ -44,7 +45,34 @@ extension type MIDIOptions._(JSObject _) implements JSObject { /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/MIDIInputMap). -extension type MIDIInputMap._(JSObject _) implements JSObject {} +extension type MIDIInputMap._(JSObject _) implements JSObject { + @JS() + external MIDIInput? get(JSString key); + @JS() + external bool has(JSString key); + @JS() + external JSIterator keys(); + Map get asMap => _MIDIInputMapMapView(this); +} + +class _MIDIInputMapMapView extends UnmodifiableMapBase { + _MIDIInputMapMapView(this._jsObject); + + final MIDIInputMap _jsObject; + + @override + MIDIInput? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + if (value == null) return null; + return value; + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } +} /// The **`MIDIOutputMap`** read-only interface of the /// [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API) @@ -59,7 +87,34 @@ extension type MIDIInputMap._(JSObject _) implements JSObject {} /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/MIDIOutputMap). -extension type MIDIOutputMap._(JSObject _) implements JSObject {} +extension type MIDIOutputMap._(JSObject _) implements JSObject { + @JS() + external MIDIOutput? get(JSString key); + @JS() + external bool has(JSString key); + @JS() + external JSIterator keys(); + Map get asMap => _MIDIOutputMapMapView(this); +} + +class _MIDIOutputMapMapView extends UnmodifiableMapBase { + _MIDIOutputMapMapView(this._jsObject); + + final MIDIOutputMap _jsObject; + + @override + MIDIOutput? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + if (value == null) return null; + return value; + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } +} /// The **`MIDIAccess`** interface of the /// [Web MIDI API](https://developer.mozilla.org/en-US/docs/Web/API/Web_MIDI_API) diff --git a/web/lib/src/dom/webrtc.dart b/web/lib/src/dom/webrtc.dart index f38fdd81..9c1303d4 100644 --- a/web/lib/src/dom/webrtc.dart +++ b/web/lib/src/dom/webrtc.dart @@ -13,6 +13,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'dom.dart'; @@ -2303,7 +2304,34 @@ extension type RTCDTMFToneChangeEventInit._(JSObject _) /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/RTCStatsReport). -extension type RTCStatsReport._(JSObject _) implements JSObject {} +extension type RTCStatsReport._(JSObject _) implements JSObject { + @JS() + external JSObject? get(JSString key); + @JS() + external bool has(JSString key); + @JS() + external JSIterator keys(); + Map get asMap => _RTCStatsReportMapView(this); +} + +class _RTCStatsReportMapView extends UnmodifiableMapBase { + _RTCStatsReportMapView(this._jsObject); + + final RTCStatsReport _jsObject; + + @override + JSObject? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key.toJS); + if (value == null) return null; + return value; + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } +} /// The **`RTCError`** interface describes an error which has occurred while /// handling diff --git a/web/lib/src/dom/webxr_hand_input.dart b/web/lib/src/dom/webxr_hand_input.dart index a8a4c047..057706d2 100644 --- a/web/lib/src/dom/webxr_hand_input.dart +++ b/web/lib/src/dom/webxr_hand_input.dart @@ -28,7 +28,15 @@ typedef XRHandJoint = String; /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/XRHand). -extension type XRHand._(JSObject _) implements JSObject {} +extension type XRHand._(JSObject _) + implements JSObject, JSIterable> { + Iterable<({String key, XRJointSpace value})> get toDart => toDartIterable.map( + (e) => ( + key: (e.toDart[0] as JSString).toDart, + value: (e.toDart[1] as XRJointSpace), + ), + ); +} /// The **`XRJointSpace`** interface is an [XRSpace] and represents the position /// and orientation of an [XRHand] joint. diff --git a/web/lib/src/dom/xhr.dart b/web/lib/src/dom/xhr.dart index 621f9bda..0155432f 100644 --- a/web/lib/src/dom/xhr.dart +++ b/web/lib/src/dom/xhr.dart @@ -14,6 +14,7 @@ @JS() library; +import 'dart:collection'; import 'dart:js_interop'; import 'dom.dart'; @@ -515,7 +516,8 @@ extension type XMLHttpRequest._(JSObject _) /// /// API documentation sourced from /// [MDN Web Docs](https://developer.mozilla.org/en-US/docs/Web/API/FormData). -extension type FormData._(JSObject _) implements JSObject { +extension type FormData._(JSObject _) + implements JSObject, JSIterable> { external factory FormData([HTMLFormElement form, HTMLElement? submitter]); /// The **`append()`** method of the [FormData] interface appends a new value @@ -556,6 +558,55 @@ extension type FormData._(JSObject _) implements JSObject { /// values with the new one, whereas `append()` will append the new value onto /// the end of the existing set of values. external void set(String name, JSAny blobValueOrValue, [String filename]); + Iterable<({String key, FormDataEntryValue value})> get toDart => + toDartIterable.map( + (e) => (key: (e.toDart[0] as JSString).toDart, value: e.toDart[1]), + ); + + @JS() + external JSIterator keys(); + Map get asMap => _FormDataMapView(this); +} + +class _FormDataMapView extends MapBase { + _FormDataMapView(this._jsObject); + + final FormData _jsObject; + + @override + FormDataEntryValue? operator [](Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key); + if (value == null) return null; + return value; + } + + @override + void operator []=(String key, FormDataEntryValue value) { + _jsObject.set(key, value); + } + + @override + void clear() { + final keys = _jsObject.keys().toDartIterable.toList(); + for (final k in keys) { + _jsObject.delete(k.toDart); + } + } + + @override + Iterable get keys { + return _jsObject.keys().toDartIterable.map((e) => e.toDart); + } + + @override + FormDataEntryValue? remove(Object? key) { + if (key is! String) return null; + final value = _jsObject.get(key); + _jsObject.delete(key); + if (value == null) return null; + return value; + } } /// The **`ProgressEvent`** interface represents events measuring progress of an diff --git a/web/pubspec.yaml b/web/pubspec.yaml index e8a9a696..43b0246f 100644 --- a/web/pubspec.yaml +++ b/web/pubspec.yaml @@ -4,7 +4,7 @@ description: Lightweight browser API bindings built around JS interop. repository: https://github.com/dart-lang/web environment: - sdk: ^3.10.0 + sdk: ^3.12.0-0 dev_dependencies: dart_flutter_team_lints: ^3.0.0 diff --git a/web/test/smoke_test.dart b/web/test/smoke_test.dart index 3a2eb667..1ffa7815 100644 --- a/web/test/smoke_test.dart +++ b/web/test/smoke_test.dart @@ -79,4 +79,153 @@ void main() { ); } }); + + test('URLSearchParams toDart works as expected.', () { + final params = URLSearchParams('a=1&b=2'.toJS); + final list = params.toDart.toList(); + expect(list.length, equals(2)); + expect(list[0].key, equals('a')); + expect(list[0].value, equals('1')); + expect(list[1].key, equals('b')); + expect(list[1].value, equals('2')); + }); + + test('FormData toDart works as expected.', () { + final form = FormData(); + form.append('a', '1'.toJS); + form.append('b', '2'.toJS); + final list = form.toDart.toList(); + expect(list.length, equals(2)); + expect(list[0].key, equals('a')); + expect((list[0].value as JSString).toDart, equals('1')); + expect(list[1].key, equals('b')); + expect((list[1].value as JSString).toDart, equals('2')); + }); + + test('NodeList toDart works as expected.', () { + final div = document.createElement('div') as HTMLDivElement; + final span1 = document.createElement('span'); + final span2 = document.createElement('span'); + div.appendChild(span1); + div.appendChild(span2); + final list = div.childNodes.toDart.toList(); + expect(list.length, equals(2)); + expect(list[0], equals(span1)); + expect(list[1], equals(span2)); + }); + + test('URLSearchParams asMap', () { + final params = URLSearchParams('a=1&b=2'.toJS); + final map = params.asMap; + + // Read operations + expect(map['a'], '1'); + expect(map['b'], '2'); + expect(map['c'], null); + expect(map.containsKey('a'), true); + expect(map.containsKey('c'), false); + expect(map.containsValue('1'), true); + expect(map.containsValue('3'), false); + expect(map.length, 2); + expect(map.isEmpty, false); + expect(map.isNotEmpty, true); + + // Keys and values + expect(map.keys.toList(), containsAll(['a', 'b'])); + expect(map.values.toList(), containsAll(['1', '2'])); + + // Write operations + map['a'] = '3'; + expect(map['a'], '3'); + expect(params.get('a'), '3'); + + map['c'] = '4'; + expect(map['c'], '4'); + expect(params.get('c'), '4'); + expect(map.length, 3); + + // Remove + expect(map.remove('b'), '2'); + expect(map.containsKey('b'), false); + expect(params.has('b'), false); + expect(map.length, 2); + + // Remove non-existent + expect(map.remove('d'), null); + + // Clear + map.clear(); + expect(map.isEmpty, true); + expect(map.length, 0); + expect(params.get('a'), null); + + // Sanity checks on Object? types for map operations + map['a'] = '1'; + final dummyJsObject = JSObject(); + final dummyDartObject = Object(); + + // operator [] should safely return null for non-Strings + // ignore: collection_methods_unrelated_type + expect(map[dummyJsObject], isNull); + expect(map[dummyDartObject], isNull); + // ignore: collection_methods_unrelated_type + expect(map[123], isNull); + + // remove should safely return null for non-Strings + // ignore: collection_methods_unrelated_type + expect(map.remove(dummyJsObject), isNull); + expect(map.remove(dummyDartObject), isNull); + // ignore: collection_methods_unrelated_type + expect(map.remove(123), isNull); + + // Ensure actual element wasn't removed accidentally + expect(map['a'], '1'); + }); + + test('StylePropertyMap asMap', () { + final div = document.createElement('div') as HTMLDivElement; + div.style.color = 'blue'; + final map = div.attributeStyleMap.asMap; + + // Read operations + expect(map.containsKey('color'), true); + final values = map['color']; + expect(values, isNotNull); + expect(values!.toDart.length, 1); + + // Remove operation + final removed = map.remove('color'); + expect(removed, isNotNull); + expect(removed!.toDart.length, 1); + expect(map.containsKey('color'), false); + expect(div.style.color, equals('')); + + // Remove non-existent + final removedAgain = map.remove('color'); + expect(removedAgain, isNull); + }); + + test('EventCounts', () { + final performance = window.performance; + expect(performance, isNotNull); + + final eventCounts = performance.eventCounts; + expect(eventCounts, isNotNull); + + final map = eventCounts.asMap; + + // We cannot easily populate EventCounts, but we can verify it is a Map + // and that it behaves as a read-only map. + expect(map, isA>()); + + // Should not throw on access. + expect(() => map.length, returnsNormally); + expect(() => map.isEmpty, returnsNormally); + expect(() => map.keys, returnsNormally); + + // Mutating operations should throw UnsupportedError. + expect(() => map['click'] = 1, throwsUnsupportedError); + expect(map.clear, throwsUnsupportedError); + expect(() => map.remove('click'), throwsUnsupportedError); + }); } diff --git a/web_generator/pubspec.yaml b/web_generator/pubspec.yaml index 95f3a884..e7cfd8f7 100644 --- a/web_generator/pubspec.yaml +++ b/web_generator/pubspec.yaml @@ -4,7 +4,7 @@ description: Generator scripts specific to package:web. repository: https://github.com/dart-lang/web environment: - sdk: ^3.10.0 + sdk: ^3.12.0-0 dependencies: args: ^2.5.0