diff --git a/ci/analyze.sh b/ci/analyze.sh index 6b84ec312ca6f..907544bdc3b2a 100755 --- a/ci/analyze.sh +++ b/ci/analyze.sh @@ -52,6 +52,7 @@ echo "" "$DART" analyze --fatal-infos --fatal-warnings "$FLUTTER_DIR/flutter_frontend_server" +(cd "$FLUTTER_DIR/tools/gen_web_locale_keymap"; "$DART" pub get) "$DART" analyze --fatal-infos --fatal-warnings "$FLUTTER_DIR/tools" (cd "$FLUTTER_DIR/testing/skia_gold_client"; "$DART" pub get) diff --git a/ci/licenses.sh b/ci/licenses.sh index f1b08f4ba8c60..87125c9afdb28 100755 --- a/ci/licenses.sh +++ b/ci/licenses.sh @@ -121,7 +121,7 @@ function verify_licenses() ( local actualLicenseCount actualLicenseCount="$(tail -n 1 flutter/ci/licenses_golden/licenses_flutter | tr -dc '0-9')" - local expectedLicenseCount=17 # When changing this number: Update the error message below as well describing all expected license types. + local expectedLicenseCount=19 # When changing this number: Update the error message below as well describing all expected license types. if [[ $actualLicenseCount -ne $expectedLicenseCount ]]; then echo "=============================== ERROR ===============================" diff --git a/ci/licenses_golden/licenses_flutter b/ci/licenses_golden/licenses_flutter index daa6e2755f882..df0c39582d599 100644 --- a/ci/licenses_golden/licenses_flutter +++ b/ci/licenses_golden/licenses_flutter @@ -1,6 +1,31 @@ UNUSED LICENSES: - +==================================================================================================== +ORIGIN: ../../../flutter/third_party/web_locale_keymap/License.txt +TYPE: LicenseType.mit +---------------------------------------------------------------------------------------------------- +MIT License + +Copyright (c) 2015 - present Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +==================================================================================================== ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ USED LICENSES: @@ -565,6 +590,35 @@ THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ==================================================================================================== +==================================================================================================== +LIBRARY: web_locale_keymap +ORIGIN: ../../../flutter/third_party/web_locale_keymap/lib/web_locale_keymap/key_mappings.g.dart + ../../../flutter/third_party/web_locale_keymap/License.txt +TYPE: LicenseType.mit +FILE: ../../../flutter/third_party/web_locale_keymap/lib/web_locale_keymap.dart +FILE: ../../../flutter/third_party/web_locale_keymap/lib/web_locale_keymap/key_mappings.g.dart +FILE: ../../../flutter/third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart +---------------------------------------------------------------------------------------------------- +Copyright (c) 2022 Google LLC + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. +==================================================================================================== + ==================================================================================================== LIBRARY: accessibility ORIGIN: ../../../flutter/third_party/accessibility/ax/ax_export.h + ../../../LICENSE @@ -3763,4 +3817,4 @@ shall not be used in advertising or otherwise to promote the sale, use or other dealings in these Data Files or Software without prior written authorization of the copyright holder. ==================================================================================================== -Total license count: 17 +Total license count: 19 diff --git a/ci/licenses_golden/tool_signature b/ci/licenses_golden/tool_signature index 3885dfddea68b..bcc22302b6d60 100644 --- a/ci/licenses_golden/tool_signature +++ b/ci/licenses_golden/tool_signature @@ -1,2 +1,2 @@ -Signature: 027af91b165acaa447651bfca8c7c704 +Signature: f6d8146c82d268e2e2549bf5019ebf07 diff --git a/lib/web_ui/lib/src/engine/keyboard_binding.dart b/lib/web_ui/lib/src/engine/keyboard_binding.dart index e7f67bcb49ce2..92d5ec4b5177b 100644 --- a/lib/web_ui/lib/src/engine/keyboard_binding.dart +++ b/lib/web_ui/lib/src/engine/keyboard_binding.dart @@ -4,6 +4,7 @@ import 'package:meta/meta.dart'; import 'package:ui/ui.dart' as ui; +import 'package:web_locale_keymap/web_locale_keymap.dart' as locale_keymap; import '../engine.dart' show registerHotRestartListener; import 'browser_detection.dart'; @@ -54,16 +55,6 @@ final Map _kLogicalKeyToModifierGetter = event.metaKey, }; -// ASCII for a, z, A, and Z -const int _kCharLowerA = 0x61; -const int _kCharLowerZ = 0x7a; -const int _kCharUpperA = 0x41; -const int _kCharUpperZ = 0x5a; -bool isAlphabet(int charCode) { - return (charCode >= _kCharLowerA && charCode <= _kCharLowerZ) - || (charCode >= _kCharUpperA && charCode <= _kCharUpperZ); -} - const String _kPhysicalCapsLock = 'CapsLock'; const String _kLogicalDead = 'Dead'; @@ -98,9 +89,24 @@ Duration _eventTimeStampToDuration(num milliseconds) { return Duration(milliseconds: ms, microseconds: micro); } +// Returns a function that caches the result of `body`, ensuring that `body` is +// only run once. +ValueGetter _cached(ValueGetter body) { + T? cache; + return () { + return cache ??= body(); + }; +} + class KeyboardBinding { KeyboardBinding._() { - _setup(); + _addEventListener('keydown', allowInterop((DomEvent domEvent) { + final FlutterHtmlKeyboardEvent event = FlutterHtmlKeyboardEvent(domEvent as DomKeyboardEvent); + return _converter.handleEvent(event); + })); + _addEventListener('keyup', allowInterop((DomEvent event) { + return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent)); + })); } /// The singleton instance of this object. @@ -117,8 +123,19 @@ class KeyboardBinding { } } + /// The platform as used in the initialization. + /// + /// By default it is derived from [operatingSystem]. + @protected + OperatingSystem get localPlatform { + return operatingSystem; + } + KeyboardConverter get converter => _converter; - late final KeyboardConverter _converter; + late final KeyboardConverter _converter = KeyboardConverter( + _onKeyData, + localPlatform, + ); final Map _listeners = {}; void _addEventListener(String eventName, DomEventListener handler) { @@ -154,16 +171,6 @@ class KeyboardBinding { return result!; } - void _setup() { - _addEventListener('keydown', allowInterop((DomEvent event) { - return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent)); - })); - _addEventListener('keyup', allowInterop((DomEvent event) { - return _converter.handleEvent(FlutterHtmlKeyboardEvent(event as DomKeyboardEvent)); - })); - _converter = KeyboardConverter(_onKeyData, onMacOs: operatingSystem == OperatingSystem.macOs); - } - void _reset() { _clearListeners(); _converter.dispose(); @@ -211,10 +218,30 @@ class FlutterHtmlKeyboardEvent { // [dispatchKeyData] as given in the constructor. Some key data might be // dispatched asynchronously. class KeyboardConverter { - KeyboardConverter(this.performDispatchKeyData, {this.onMacOs = false}); + KeyboardConverter(this.performDispatchKeyData, OperatingSystem platform) + : onMacOs = platform == OperatingSystem.macOs, + _mapping = _mappingFromPlatform(platform); final DispatchKeyData performDispatchKeyData; + /// Whether the current platform is macOS, which affects how certain key events + /// are comprehended. final bool onMacOs; + /// Maps logical keys from key event properties. + final locale_keymap.LocaleKeymap _mapping; + + static locale_keymap.LocaleKeymap _mappingFromPlatform(OperatingSystem platform) { + switch (platform) { + case OperatingSystem.iOs: + case OperatingSystem.macOs: + return locale_keymap.LocaleKeymap.darwin(); + case OperatingSystem.windows: + return locale_keymap.LocaleKeymap.win(); + case OperatingSystem.android: + case OperatingSystem.linux: + case OperatingSystem.unknown: + return locale_keymap.LocaleKeymap.linux(); + } + } // The `performDispatchKeyData` wrapped with tracking logic. // @@ -273,29 +300,14 @@ class KeyboardConverter { (metaDown ? _kDeadKeyMeta : 0); } - // Whether `event.key` should be considered a key name. + // Whether `event.key` is a key name, such as "Shift", or otherwise a + // character, such as "S" or "ж". // - // The `event.key` can either be a key name or the printable character. If the - // first character is an alphabet, it must be either 'A' to 'Z' ( and return - // true), or be a key name (and return false). Otherwise, return true. - static bool _eventKeyIsKeyname(String key) { - assert(key.isNotEmpty); - return isAlphabet(key.codeUnitAt(0)) && key.length > 1; - } - - static int _characterToLogicalKey(String key) { - // Assume the length being <= 2 to be sufficient in all cases. If not, - // extend the algorithm. - assert(key.length <= 2); - int result = key.codeUnitAt(0) & 0xffff; - if (key.length == 2) { - result += key.codeUnitAt(1) << 16; - } - // Convert upper letters to lower letters - if (result >= _kCharUpperA && result <= _kCharUpperZ) { - result = result + _kCharLowerA - _kCharUpperA; - } - return result; + // A key name always has more than 1 code unit, and they are all alnums. + // Character keys, however, can also have more than 1 code unit: en-in + // maps KeyL to L̥/l̥. To resolve this, we check the second code unit. + static bool _eventKeyIsKeyName(String key) { + return key.length > 1 && key.codeUnitAt(0) < 0x7F && key.codeUnitAt(1) < 0x7F; } static int _deadKeyToLogicalKey(int physicalKey, FlutterHtmlKeyboardEvent event) { @@ -307,10 +319,6 @@ class KeyboardConverter { return physicalKey + _getModifierMask(event) + _kWebKeyIdPlane; } - static int _otherLogicalKey(String key) { - return kWebToLogicalKey[key] ?? (key.hashCode + _kWebKeyIdPlane); - } - // Map from pressed physical key to corresponding pressed logical key. // // Multiple physical keys can be mapped to the same logical key, usually due @@ -369,22 +377,36 @@ class KeyboardConverter { final String eventKey = event.key!; final int physicalKey = _getPhysicalCode(event.code!); - final bool logicalKeyIsCharacter = !_eventKeyIsKeyname(eventKey); - final String? character = logicalKeyIsCharacter ? eventKey : null; - final int logicalKey = () { + final bool logicalKeyIsCharacter = !_eventKeyIsKeyName(eventKey); + // The function body might or might not be evaluated. If the event is a key + // up event, the resulting event will simply use the currently pressed + // logical key. + final ValueGetter logicalKey = _cached(() { + // Mapped logical keys, such as ArrowLeft, Escape, AudioVolumeDown. + final int? mappedLogicalKey = kWebToLogicalKey[eventKey]; + if (mappedLogicalKey != null) { + return mappedLogicalKey; + } + // Keys with locations, such as modifier keys (Shift) or numpad keys. if (kWebLogicalLocationMap.containsKey(event.key)) { final int? result = kWebLogicalLocationMap[event.key!]?[event.location!]; assert(result != null, 'Invalid modifier location: ${event.key}, ${event.location}'); return result!; } - if (character != null) { - return _characterToLogicalKey(character); + // Locale-sensitive keys: letters, digits, and certain symbols. + if (logicalKeyIsCharacter) { + final int? localeLogicalKeys = _mapping.getLogicalKey(event.code, event.key, event.keyCode); + if (localeLogicalKeys != null) { + return localeLogicalKeys; + } } + // Dead keys that are not handled by the locale mapping. if (eventKey == _kLogicalDead) { return _deadKeyToLogicalKey(physicalKey, event); } - return _otherLogicalKey(eventKey); - }(); + // Minted logical keys. + return eventKey.hashCode + _kWebKeyIdPlane; + }); assert(event.type == 'keydown' || event.type == 'keyup'); final bool isPhysicalDown = event.type == 'keydown' || @@ -406,7 +428,7 @@ class KeyboardConverter { timeStamp: timeStamp, type: ui.KeyEventType.up, physical: physicalKey, - logical: logicalKey, + logical: logicalKey(), character: null, synthesized: true, ), @@ -441,7 +463,7 @@ class KeyboardConverter { timeStamp: timeStamp, type: ui.KeyEventType.up, physical: physicalKey, - logical: logicalKey, + logical: logicalKey(), character: null, synthesized: true, )); @@ -474,7 +496,7 @@ class KeyboardConverter { switch (type) { case ui.KeyEventType.down: assert(lastLogicalRecord == null); - nextLogicalRecord = logicalKey; + nextLogicalRecord = logicalKey(); break; case ui.KeyEventType.up: assert(lastLogicalRecord != null); @@ -499,7 +521,7 @@ class KeyboardConverter { _kLogicalKeyToModifierGetter.forEach((int testeeLogicalKey, _ModifierGetter getModifier) { // Do not synthesize for the key of the current event. The event is the // ground truth. - if (logicalKey == testeeLogicalKey) { + if (logicalKey() == testeeLogicalKey) { return; } if (_pressingRecords.containsValue(testeeLogicalKey) && !getModifier(event)) { @@ -525,17 +547,18 @@ class KeyboardConverter { // Update key guards if (logicalKeyIsCharacter) { if (nextLogicalRecord != null) { - _startGuardingKey(physicalKey, logicalKey, timeStamp); + _startGuardingKey(physicalKey, logicalKey(), timeStamp); } else { _stopGuardingKey(physicalKey); } } + final String? character = logicalKeyIsCharacter ? eventKey : null; final ui.KeyData keyData = ui.KeyData( timeStamp: timeStamp, type: type, physical: physicalKey, - logical: lastLogicalRecord ?? logicalKey, + logical: lastLogicalRecord ?? logicalKey(), character: type == ui.KeyEventType.up ? null : character, synthesized: false, ); diff --git a/lib/web_ui/pubspec.yaml b/lib/web_ui/pubspec.yaml index 7d280a9a7c239..cf0233c8b85d4 100644 --- a/lib/web_ui/pubspec.yaml +++ b/lib/web_ui/pubspec.yaml @@ -8,6 +8,8 @@ environment: dependencies: js: 0.6.4 meta: ^1.7.0 + web_locale_keymap: + path: ../../third_party/web_locale_keymap web_unicode: path: ../../third_party/web_unicode diff --git a/lib/web_ui/test/engine/pointer_binding_test.dart b/lib/web_ui/test/engine/pointer_binding_test.dart index fd4cca8cab730..88c02a05a3ac9 100644 --- a/lib/web_ui/test/engine/pointer_binding_test.dart +++ b/lib/web_ui/test/engine/pointer_binding_test.dart @@ -52,7 +52,7 @@ void testMain() { return KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); } test('ios workaround', () { diff --git a/lib/web_ui/test/keyboard_converter_test.dart b/lib/web_ui/test/keyboard_converter_test.dart index 53dba7f2e8d7a..3bbbeb8bc6be3 100644 --- a/lib/web_ui/test/keyboard_converter_test.dart +++ b/lib/web_ui/test/keyboard_converter_test.dart @@ -18,6 +18,7 @@ const int kLocationNumpad = 3; final int kPhysicalKeyA = kWebToPhysicalKey['KeyA']!; final int kPhysicalKeyE = kWebToPhysicalKey['KeyE']!; +final int kPhysicalKeyL = kWebToPhysicalKey['KeyL']!; final int kPhysicalKeyU = kWebToPhysicalKey['KeyU']!; final int kPhysicalDigit1 = kWebToPhysicalKey['Digit1']!; final int kPhysicalNumpad1 = kWebToPhysicalKey['Numpad1']!; @@ -31,6 +32,7 @@ final int kPhysicalScrollLock = kWebToPhysicalKey['ScrollLock']!; const int kPhysicalEmptyCode = 0x1700000000; const int kLogicalKeyA = 0x00000000061; +const int kLogicalKeyL = 0x0000000006C; const int kLogicalKeyU = 0x00000000075; const int kLogicalDigit1 = 0x00000000031; final int kLogicalNumpad1 = kWebLogicalLocationMap['1']![kLocationNumpad]!; @@ -85,7 +87,7 @@ void testMain() { keyDataList.add(key); // Only handle down events return key.type == ui.KeyEventType.down; - }); + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('KeyA', 'a')..timeStamp = 1); expectKeyData(keyDataList.last, @@ -128,13 +130,32 @@ void testMain() { expect(MockKeyboardEvent.lastDefaultPrevented, isFalse); }); + test('Special cases', () { + final List keyDataList = []; + final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { + keyDataList.add(key); + // Only handle down events + return key.type == ui.KeyEventType.down; + }, OperatingSystem.windows); + + // en-in.win, with AltGr + converter.handleEvent(keyDownEvent('KeyL', 'l̥', kCtrl | kAlt)..timeStamp = 1); + expectKeyData(keyDataList.last, + timeStamp: const Duration(milliseconds: 1), + type: ui.KeyEventType.down, + physical: kPhysicalKeyL, + logical: kLogicalKeyL, + character: 'l̥', + ); + }); + test('Release modifier during a repeated sequence', () { final List keyDataList = []; final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); // Only handle down events return key.type == ui.KeyEventType.down; - }); + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)); expectKeyData(keyDataList.last, @@ -205,7 +226,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)); expectKeyData(keyDataList.last, @@ -245,7 +266,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('', 'Shift', kShift)); expectKeyData(keyDataList.last, @@ -317,7 +338,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('Digit1', '1')); expectKeyData(keyDataList.last, @@ -357,7 +378,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); // The absolute values of the following logical keys are not guaranteed. const int kLogicalAltE = 0x1740070008; @@ -431,7 +452,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)); expect(MockKeyboardEvent.lastDefaultPrevented, isTrue); @@ -472,7 +493,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); // A KeyDown of ShiftRight is missed due to loss of focus. converter.handleEvent(keyUpEvent('ShiftRight', 'Shift', 0, kLocationRight)); @@ -487,7 +508,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); // Same layout converter.handleEvent(keyDownEvent('KeyA', 'a')); @@ -525,7 +546,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }, onMacOs: true); + }, OperatingSystem.macOs); // A KeyDown of ShiftRight is missed due to loss of focus. converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock')); @@ -597,7 +618,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); // onMacOs: false + }, OperatingSystem.linux); // onMacOs: false converter.handleEvent(keyDownEvent('CapsLock', 'CapsLock')); expect(keyDataList, hasLength(1)); @@ -647,7 +668,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }, onMacOs: true); + }, OperatingSystem.macOs); converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100); async.elapse(const Duration(milliseconds: 100)); @@ -711,7 +732,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }, onMacOs: true); + }, OperatingSystem.macOs); converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100); async.elapse(const Duration(milliseconds: 100)); @@ -774,7 +795,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100); async.elapse(const Duration(milliseconds: 100)); @@ -831,7 +852,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); // onMacOs: false + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('MetaLeft', 'Meta', kMeta, kLocationLeft)..timeStamp = 100); async.elapse(const Duration(milliseconds: 100)); @@ -855,7 +876,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); // onMacOs: false + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('ScrollLock', 'ScrollLock')); expect(keyDataList, hasLength(1)); @@ -902,7 +923,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); // onMacOs: false + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('ShiftRight', 'Shift', kShift, kLocationRight)); expectKeyData(keyDataList.last, @@ -958,7 +979,7 @@ void testMain() { final KeyboardConverter converter = KeyboardConverter((ui.KeyData key) { keyDataList.add(key); return true; - }); // onMacOs: false + }, OperatingSystem.linux); converter.handleEvent(keyDownEvent('ShiftLeft', 'Shift', kShift, kLocationLeft)); expectKeyData(keyDataList.last, diff --git a/third_party/web_locale_keymap/CHANGELOG.md b/third_party/web_locale_keymap/CHANGELOG.md new file mode 100644 index 0000000000000..13854562d7059 --- /dev/null +++ b/third_party/web_locale_keymap/CHANGELOG.md @@ -0,0 +1,3 @@ +## 0.1.1 + +* Initial release. diff --git a/third_party/web_locale_keymap/License.txt b/third_party/web_locale_keymap/License.txt new file mode 100644 index 0000000000000..0ac28ee234d23 --- /dev/null +++ b/third_party/web_locale_keymap/License.txt @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2015 - present Microsoft Corporation + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/third_party/web_locale_keymap/README.md b/third_party/web_locale_keymap/README.md new file mode 100644 index 0000000000000..b327606d73b51 --- /dev/null +++ b/third_party/web_locale_keymap/README.md @@ -0,0 +1,38 @@ +# Web Locale Keymap + +This package maps Web's KeyboardEvent to Flutter's logical keys. It only +includes "locale sensitive" keys, which are all the letters, regular digits, and +symbols. It works for all layouts shown in [Microsoft/VSCode](https://github.com/microsoft/vscode) repo. + +The mapping data and test cases are generated by `gen_web_locale_keymap` package. +Do not edit them manually. + +# Usage + +1. Ensure that the key is a locale key. +2. Get the logical key from `getLogicalKey()`. +3. If the return value is null, then the key can not be mapped as a character +either. Mint the logical key value. + +```dart +import 'package:web_locale_keymap/web_locale_keymap.dart' as locale_keymap; + +final locale_keymap.LocaleKeymap mapping = + locale_keymap.LocaleKeymap.win(); // Or .darwin() or .linux() + +/* ... */ + +int getLogicalKey(html.KeyboardEvent event) { + int? result = _convertToDeadKey(event) + ?? _convertToUnprintableKey(event) + ?? _convertToNumpadKey(event); + if (result != null) { + return result; + } + result = mapping.getLogicalKey(event.code, event.key, event.keyCode); + if (result != null) { + return result; + } + return _mintLogicalKey(event); +} +``` diff --git a/third_party/web_locale_keymap/lib/web_locale_keymap.dart b/third_party/web_locale_keymap/lib/web_locale_keymap.dart new file mode 100644 index 0000000000000..eb120ee7ed405 --- /dev/null +++ b/third_party/web_locale_keymap/lib/web_locale_keymap.dart @@ -0,0 +1,9 @@ +//--------------------------------------------------------------------------------------------- +// Copyright (c) 2022 Google LLC +// Licensed under the MIT License. See License.txt in the project root for license information. +//--------------------------------------------------------------------------------------------*/ + +library web_locale_keymap; + +export 'web_locale_keymap/key_mappings.g.dart'; +export 'web_locale_keymap/locale_keymap.dart'; diff --git a/third_party/web_locale_keymap/lib/web_locale_keymap/key_mappings.g.dart b/third_party/web_locale_keymap/lib/web_locale_keymap/key_mappings.g.dart new file mode 100644 index 0000000000000..cae2917ff0a52 --- /dev/null +++ b/third_party/web_locale_keymap/lib/web_locale_keymap/key_mappings.g.dart @@ -0,0 +1,273 @@ +//--------------------------------------------------------------------------------------------- +// Copyright (c) 2022 Google LLC +// Licensed under the MIT License. See License.txt in the project root for license information. +//--------------------------------------------------------------------------------------------*/ + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// +// This file is auto generated by flutter/engine:flutter/tools/gen_web_keyboard_layouts based on +// https://github.com/microsoft/vscode/tree/ab7ccc9e872dfcdfc429f8f2815109ec0ca926e3/src/vs/workbench/services/keybinding/browser/keyboardLayouts +// +// Edit the following files instead: +// +// - Script: lib/main.dart +// - Templates: data/*.tmpl +// +// See flutter/engine:flutter/tools/gen_web_keyboard_layouts/README.md for more information. + +/// Used in the final mapping indicating the logical key should be derived from +/// KeyboardEvent.keyCode. +/// +/// This value is chosen because it's a printable character within EASCII that +/// will never be mapped to (checked in the marshalling algorithm). +const int kUseKeyCode = 0xFF; + +/// Used in the final mapping indicating the event key is 'Dead', the dead key. +final String _kUseDead = String.fromCharCode(0xFE); + +/// The KeyboardEvent.key for a dead key. +const String _kEventKeyDead = 'Dead'; + +/// A map of all goals from the scan codes to their mapped value in US layout. +const Map kLayoutGoals = { + 'KeyA': 'a', + 'KeyB': 'b', + 'KeyC': 'c', + 'KeyD': 'd', + 'KeyE': 'e', + 'KeyF': 'f', + 'KeyG': 'g', + 'KeyH': 'h', + 'KeyI': 'i', + 'KeyJ': 'j', + 'KeyK': 'k', + 'KeyL': 'l', + 'KeyM': 'm', + 'KeyN': 'n', + 'KeyO': 'o', + 'KeyP': 'p', + 'KeyQ': 'q', + 'KeyR': 'r', + 'KeyS': 's', + 'KeyT': 't', + 'KeyU': 'u', + 'KeyV': 'v', + 'KeyW': 'w', + 'KeyX': 'x', + 'KeyY': 'y', + 'KeyZ': 'z', + 'Digit1': '1', + 'Digit2': '2', + 'Digit3': '3', + 'Digit4': '4', + 'Digit5': '5', + 'Digit6': '6', + 'Digit7': '7', + 'Digit8': '8', + 'Digit9': '9', + 'Digit0': '0', + 'Minus': '-', + 'Equal': '=', + 'BracketLeft': '[', + 'BracketRight': ']', + 'Backslash': r'\', + 'Semicolon': ';', + 'Quote': "'", + 'Backquote': '`', + 'Comma': ',', + 'Period': '.', + 'Slash': '/', +}; + +final int _kLowerA = 'a'.codeUnitAt(0); +final int _kUpperA = 'A'.codeUnitAt(0); +final int _kLowerZ = 'z'.codeUnitAt(0); +final int _kUpperZ = 'Z'.codeUnitAt(0); + +bool _isAscii(int charCode) { + // 0x20 is the first printable character in ASCII. + return charCode >= 0x20 && charCode <= 0x7F; +} + +/// Returns whether the `char` is a single character of a letter or a digit. +bool isLetter(int charCode) { + return (charCode >= _kLowerA && charCode <= _kLowerZ) + || (charCode >= _kUpperA && charCode <= _kUpperZ); +} + +/// A set of rules that can derive a large number of logical keys simply from +/// the event's code and key. +/// +/// This greatly reduces the entries needed in the final mapping. +int? heuristicMapper(String code, String key) { + // Digit code: return the digit. + if (code.startsWith('Digit')) { + assert(code.length == 6); + return code.codeUnitAt(5); // The character immediately after 'Digit' + } + final int charCode = key.codeUnitAt(0); + if (key.length > 1 || !_isAscii(charCode)) { + return kLayoutGoals[code]?.codeUnitAt(0); + } + // Letter key: return the letter. + if (isLetter(charCode)) { + return key.toLowerCase().codeUnitAt(0); + } + return null; +} + +// Maps an integer to a printable EASCII character by adding it to this value. +// +// We could've chosen 0x20, the first printable character, for a slightly bigger +// range, but it's prettier this way and sufficient. +final int _kMarshallIntBase = '0'.codeUnitAt(0); + +class _StringStream { + _StringStream(this._data) : _offset = 0; + + final String _data; + final Map _goalToEventCode = Map.fromEntries( + kLayoutGoals + .entries + .map((MapEntry beforeEntry) => + MapEntry(beforeEntry.value.codeUnitAt(0), beforeEntry.key)) + ); + + int get offest => _offset; + int _offset; + + int readIntAsVerbatim() { + final int result = _data.codeUnitAt(_offset); + _offset += 1; + assert(result >= _kMarshallIntBase); + return result - _kMarshallIntBase; + } + + int readIntAsChar() { + final int result = _data.codeUnitAt(_offset); + _offset += 1; + return result; + } + + String readEventKey() { + final String char = String.fromCharCode(readIntAsChar()); + if (char == _kUseDead) { + return _kEventKeyDead; + } else { + return char; + } + } + + String readEventCode() { + final int charCode = _data.codeUnitAt(_offset); + _offset += 1; + return _goalToEventCode[charCode]!; + } +} + +Map _unmarshallCodeMap(_StringStream stream) { + final int entryNum = stream.readIntAsVerbatim(); + return Map.fromEntries((() sync* { + for (int entryIndex = 0; entryIndex < entryNum; entryIndex += 1) { + yield MapEntry(stream.readEventKey(), stream.readIntAsChar()); + } + })()); +} + +/// Decode a key mapping data out of the string. +Map> unmarshallMappingData(String compressed) { + final _StringStream stream = _StringStream(compressed); + final int eventCodeNum = stream.readIntAsVerbatim(); + return Map>.fromEntries((() sync* { + for (int eventCodeIndex = 0; eventCodeIndex < eventCodeNum; eventCodeIndex += 1) { + yield MapEntry>(stream.readEventCode(), _unmarshallCodeMap(stream)); + } + })()); +} + +/// Data for [LocaleKeymap] on Windows. +/// +/// Do not use this value, but [LocaleKeymap.win] instead. +/// +/// The keys are `KeyboardEvent.code` and then `KeyboardEvent.key`. The values +/// are logical keys or [kUseKeyCode]. Entries that can be derived using +/// heuristics have been omitted. +Map> getMappingDataWin() { + return unmarshallMappingData( + r';' + r'b1{b' + r'c1&c' + r'f1[f' + r'g1]g' + r'm2y' + ); // 59 characters +} + +/// Data for [LocaleKeymap] on Linux. +/// +/// Do not use this value, but [LocaleKeymap.linux] instead. +/// +/// The keys are `KeyboardEvent.code` and then `KeyboardEvent.key`. The values +/// are logical keys or [kUseKeyCode]. Entries that can be derived using +/// heuristics have been omitted. +Map> getMappingDataLinux() { + return unmarshallMappingData( + r'8' + r'a2@qΩq' + r'k1&k' + r'q3@qÆaæa' + r'w2x' + r'y2¥ÿ←ÿ' + r'z5> getMappingDataDarwin() { + return unmarshallMappingData( + r'M' + r',2„w∑w' + r'a2Ωq‡q' + r'b2˛x≈x' + r'c3 cÔj∆j' + r'd2þe´e' + r'f2þu¨u' + r'g2þÿˆi' + r'h3 hÎÿ∂d' + r'i3 iÇcçc' + r'j2Óh˙h' + r'k2ˇÿ†t' + r'l5 l@lþÿ|l˜n' + r'm1~m' + r'n3 nıÿ∫b' + r'o2®r‰r' + r'p2¬lÒl' + r'q2Æaæa' + r'r3 rπp∏p' + r's3 sØoøo' + r't2¥yÁy' + r'u3 u©g˝g' + r'v2˚kk' + r'w2ÂzÅz' + r'x2Œqœq' + r'y5 yÏfƒfˇzΩz' + r'z5 z¥y‡y‹ÿ›w' + r'.2√v◊v' + r';4µmÍsÓmßs' + r'/2¸zΩz' + ); // 209 characters +} diff --git a/third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart b/third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart new file mode 100644 index 0000000000000..dd2faa527fc2a --- /dev/null +++ b/third_party/web_locale_keymap/lib/web_locale_keymap/locale_keymap.dart @@ -0,0 +1,63 @@ +//--------------------------------------------------------------------------------------------- +// Copyright (c) 2022 Google LLC +// Licensed under the MIT License. See License.txt in the project root for license information. +//--------------------------------------------------------------------------------------------*/ + +import 'key_mappings.g.dart'; + +int? _characterToLogicalKey(String? key) { + // We have yet to find a case where length >= 2 is useful. + if (key == null || key.length >= 2) { + return null; + } + final int result = key.toLowerCase().codeUnitAt(0); + return result; +} + +/// Maps locale-sensitive keys from KeyboardEvent properties to a logical key. +class LocaleKeymap { + /// Create a [LocaleKeymap] for Windows. + LocaleKeymap.win() : _mapping = getMappingDataWin(); + + /// Create a [LocaleKeymap] for Linux. + LocaleKeymap.linux() : _mapping = getMappingDataLinux(); + + /// Create a [LocaleKeymap] for Darwin. + LocaleKeymap.darwin() : _mapping = getMappingDataDarwin(); + + /// Return a logical key mapped from KeyboardEvent properties. + /// + /// This method handles all printable characters, including letters, digits, + /// and symbols. + /// + /// Before calling this method, the caller should have eliminated cases where + /// the event key is a "key name", such as "Shift" or "AudioVolumnDown". + /// + /// If the return value is null, there's no way to derive a meaningful value + /// from the printable information of the event. + int? getLogicalKey(String? eventCode, String? eventKey, int eventKeyCode) { + final int? result = _mapping[eventCode]?[eventKey]; + if (result == kUseKeyCode) { + return eventKeyCode; + } + if (result == null) { + final int? heuristicResult = heuristicMapper(eventCode ?? '', eventKey ?? ''); + if (heuristicResult != null) { + return heuristicResult; + } + // Characters: map to unicode zone. + // + // While characters are usually resolved in the last step, this can happen + // in non-latin layouts when a non-latin character is on a symbol key (ru, + // Semicolon-ж) or on an alnum key that has been assigned elsewhere (hu, + // Digit0-Ö). + final int? characterLogicalKey = _characterToLogicalKey(eventKey); + if (characterLogicalKey != null) { + return characterLogicalKey; + } + } + return result; + } + + final Map> _mapping; +} diff --git a/third_party/web_locale_keymap/pubspec.yaml b/third_party/web_locale_keymap/pubspec.yaml new file mode 100644 index 0000000000000..c9c7518d01ed1 --- /dev/null +++ b/third_party/web_locale_keymap/pubspec.yaml @@ -0,0 +1,9 @@ +name: web_locale_keymap + +publish_to: none + +environment: + sdk: ">=2.12.0 <3.0.0" + +dev_dependencies: + test: ^1.21.7 diff --git a/third_party/web_locale_keymap/test/layout_mapping_test.dart b/third_party/web_locale_keymap/test/layout_mapping_test.dart new file mode 100644 index 0000000000000..2197bf2e46b0c --- /dev/null +++ b/third_party/web_locale_keymap/test/layout_mapping_test.dart @@ -0,0 +1,24 @@ +//--------------------------------------------------------------------------------------------- +// Copyright (c) 2022 Google LLC +// Licensed under the MIT License. See License.txt in the project root for license information. +//--------------------------------------------------------------------------------------------*/ + + +import 'package:test/test.dart'; +import 'package:web_locale_keymap/web_locale_keymap.dart'; + +import 'test_cases.g.dart'; + +void main() { + group('Win', () { + testWin(LocaleKeymap.win()); + }); + + group('Linux', () { + testLinux(LocaleKeymap.linux()); + }); + + group('Darwin', () { + testDarwin(LocaleKeymap.darwin()); + }); +} diff --git a/third_party/web_locale_keymap/test/test_cases.g.dart b/third_party/web_locale_keymap/test/test_cases.g.dart new file mode 100644 index 0000000000000..b57842551d497 --- /dev/null +++ b/third_party/web_locale_keymap/test/test_cases.g.dart @@ -0,0 +1,1302 @@ +//--------------------------------------------------------------------------------------------- +// Copyright (c) 2022 Google LLC +// Licensed under the MIT License. See License.txt in the project root for license information. +//--------------------------------------------------------------------------------------------*/ + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// +// This file is auto generated by flutter/engine:flutter/tools/gen_web_keyboard_layouts based on +// https://github.com/microsoft/vscode/tree/@@@COMMIT_ID@@@/src/vs/workbench/services/keybinding/browser/keyboardLayouts +// +// Edit the following files instead: +// +// - Script: lib/main.dart +// - Templates: data/*.tmpl +// +// See flutter/engine:flutter/tools/gen_web_keyboard_layouts/README.md for more information. + +import 'package:test/test.dart'; +import 'package:web_locale_keymap/web_locale_keymap.dart'; +import 'testing.dart'; + +void testWin(LocaleKeymap mapping) { + group('cz', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'{', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'&', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'Đ', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'[', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r']', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'ł', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'Ł', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'}', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'\', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'đ', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'@', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'|', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'#', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'z', r'Z', r'', r''], 'z'); + verifyEntry(mapping, 'KeyZ', [r'y', r'Y', r'', r''], 'y'); + }); + + group('de', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'@', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'z', r'Z', r'', r''], 'z'); + verifyEntry(mapping, 'KeyZ', [r'y', r'Y', r'', r''], 'y'); + }); + + group('de-swiss', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'z', r'Z', r'', r''], 'z'); + verifyEntry(mapping, 'KeyZ', [r'y', r'Y', r'', r''], 'y'); + }); + + group('dk', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('en', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('en-belgian', () { + verifyEntry(mapping, 'KeyA', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'z', r'Z', r'', r''], 'z'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'Semicolon', [r'm', r'M', r'', r''], 'm'); + }); + + group('en-in', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'ā', r'Ā'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'ḍ', r'Ḍ'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'ē', r'Ē'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'ṅ', r'Ṅ'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'ḥ', r'Ḥ'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'ī', r'Ī'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'l̥', r'L̥'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'ṁ', r'Ṁ'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'ṇ', r'Ṇ'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ō', r'Ō'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'æ', r'Æ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'r̥', r'R̥'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ś', r'Ś'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'ṭ', r'Ṭ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'ū', r'Ū'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'ṣ', r'Ṣ'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'ñ', r'Ñ'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('en-intl', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'á', r'Á'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'©', r'¢'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'ð', r'Ð'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'é', r'É'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'í', r'Í'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'ø', r'Ø'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'ñ', r'Ñ'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ó', r'Ó'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'ö', r'Ö'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'ä', r'Ä'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'§'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'þ', r'Þ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'ú', r'Ú'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'å', r'Å'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'ü', r'Ü'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'æ', r'Æ'], 'z'); + }); + + group('en-uk', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'á', r'Á'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'é', r'É'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'í', r'Í'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ó', r'Ó'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'ú', r'Ú'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('es', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('es-latin', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'@', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('fr', () { + verifyEntry(mapping, 'KeyA', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'z', r'Z', r'', r''], 'z'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'Semicolon', [r'm', r'M', r'', r''], 'm'); + }); + + group('hu', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'ä', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'{', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'&', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'Đ', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Ä', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'[', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r']', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Í', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'í', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'ł', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'Ł', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'<', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'}', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'\', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'đ', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'€', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'@', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'|', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'#', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'z', r'Z', r'', r''], 'z'); + verifyEntry(mapping, 'KeyZ', [r'y', r'Y', r'>', r''], 'y'); + }); + + group('it', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('no', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('pl', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'ą', r'Ą'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ć', r'Ć'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'ę', r'Ę'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'ł', r'Ł'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'ń', r'Ń'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ó', r'Ó'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ś', r'Ś'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'€', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'ź', r'Ź'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'ż', r'Ż'], 'z'); + }); + + group('pt', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('pt-br', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'₢', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'°', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'/', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'?', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('ru', () { + verifyEntry(mapping, 'KeyA', [r'ф', r'Ф', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'и', r'И', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'с', r'С', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'в', r'В', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'у', r'У', r'', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'а', r'А', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'п', r'П', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'р', r'Р', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'ш', r'Ш', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'о', r'О', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'л', r'Л', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'д', r'Д', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'ь', r'Ь', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'т', r'Т', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'щ', r'Щ', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'з', r'З', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'й', r'Й', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'к', r'К', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r'ы', r'Ы', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r'е', r'Е', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'г', r'Г', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'м', r'М', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'ц', r'Ц', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'ч', r'Ч', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'н', r'Н', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'я', r'Я', r'', r''], 'z'); + }); + + group('sv', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); + + group('thai', () { + verifyEntry(mapping, 'KeyA', [r'ฟ', r'ฤ', r'', r''], 'a'); + verifyEntry(mapping, 'KeyB', [r'ิ', r'ฺ', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'แ', r'ฉ', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'ก', r'ฏ', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'ำ', r'ฎ', r'', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'ด', r'โ', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'เ', r'ฌ', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'้', r'็', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'ร', r'ณ', r'', r''], 'i'); + verifyEntry(mapping, 'KeyJ', [r'่', r'๋', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'า', r'ษ', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'ส', r'ศ', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'ท', r'?', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'ื', r'์', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'น', r'ฯ', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'ย', r'ญ', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'ๆ', r'๐', r'', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'พ', r'ฑ', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r'ห', r'ฆ', r'', r''], 's'); + verifyEntry(mapping, 'KeyT', [r'ะ', r'ธ', r'', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'ี', r'๊', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'อ', r'ฮ', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'ไ', r'"', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'ป', r')', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'ั', r'ํ', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'ผ', r'(', r'', r''], 'z'); + }); + + group('tr', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'æ', r'Æ'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'', r''], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'', r''], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r''], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r''], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'', r''], 'h'); + verifyEntry(mapping, 'KeyI', [r'ı', r'I', r'i', r'İ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'', r''], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'', r''], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'', r''], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'', r''], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'', r''], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'', r''], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'@', r''], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'', r''], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'₺', r''], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'', r''], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'', r''], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'', r''], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'', r''], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'', r''], 'z'); + }); +} + +void testLinux(LocaleKeymap mapping) { + group('de', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'æ', r'Æ'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'“', r'‘'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'¢', r'©'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'ð', r'Ð'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r'€'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'đ', r'ª'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'ŋ', r'Ŋ'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'ħ', r'Ħ'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'→', r'ı'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'̣', r'̇'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'ĸ', r'&'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'ł', r'Ł'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'º'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'”', r'’'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'þ', r'Þ'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'@', r'Ω'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'¶', r'®'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ſ', r'ẞ'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'ŧ', r'Ŧ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'↓', r'↑'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'„', r'‚'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'ł', r'Ł'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'«', r'‹'], 'x'); + verifyEntry(mapping, 'KeyY', [r'z', r'Z', r'←', r'¥'], 'z'); + verifyEntry(mapping, 'KeyZ', [r'y', r'Y', r'»', r'›'], 'y'); + }); + + group('en', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'a', r'A'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'b', r'B'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'c', r'C'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'd', r'D'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'e', r'E'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'f', r'F'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'g', r'G'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'h', r'H'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'i', r'I'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'j', r'J'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'k', r'K'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'l', r'L'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'm', r'M'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'n', r'N'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'o', r'O'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'p', r'P'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'q', r'Q'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'r', r'R'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r's', r'S'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r't', r'T'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'u', r'U'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'v', r'V'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'w', r'W'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'x', r'X'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'y', r'Y'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'z', r'Z'], 'z'); + }); + + group('es', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'æ', r'Æ'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'”', r'’'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'¢', r'©'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'ð', r'Ð'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r'¢'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'đ', r'ª'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'ŋ', r'Ŋ'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'ħ', r'Ħ'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'→', r'ı'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'̉', r'̛'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'ĸ', r'&'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'ł', r'Ł'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'º'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'n', r'N'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'þ', r'Þ'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'@', r'Ω'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'¶', r'®'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'§'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'ŧ', r'Ŧ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'↓', r'↑'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'“', r'‘'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'ł', r'Ł'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'»', r'>'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'←', r'¥'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'«', r'<'], 'z'); + }); + + group('fr', () { + verifyEntry(mapping, 'KeyA', [r'q', r'Q', r'@', r'Ω'], 'q'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'”', r'’'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'¢', r'©'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'ð', r'Ð'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r'¢'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'đ', r'ª'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'ŋ', r'Ŋ'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'ħ', r'Ħ'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'→', r'ı'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'̉', r'̛'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'ĸ', r'&'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'ł', r'Ł'], 'l'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'n', r'N'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'þ', r'Þ'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'a', r'A', r'æ', r'Æ'], 'a'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'¶', r'®'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'§'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'ŧ', r'Ŧ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'↓', r'↑'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'“', r'‘'], 'v'); + verifyEntry(mapping, 'KeyW', [r'z', r'Z', r'«', r'<'], 'z'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'»', r'>'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'←', r'¥'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'w', r'W', r'ł', r'Ł'], 'w'); + verifyEntry(mapping, 'Semicolon', [r'm', r'M', r'µ', r'º'], 'm'); + }); + + group('ru', () { + verifyEntry(mapping, 'KeyA', [r'ф', r'Ф', r'ф', r'Ф'], 'a'); + verifyEntry(mapping, 'KeyB', [r'и', r'И', r'и', r'И'], 'b'); + verifyEntry(mapping, 'KeyC', [r'с', r'С', r'с', r'С'], 'c'); + verifyEntry(mapping, 'KeyD', [r'в', r'В', r'в', r'В'], 'd'); + verifyEntry(mapping, 'KeyE', [r'у', r'У', r'у', r'У'], 'e'); + verifyEntry(mapping, 'KeyF', [r'а', r'А', r'а', r'А'], 'f'); + verifyEntry(mapping, 'KeyG', [r'п', r'П', r'п', r'П'], 'g'); + verifyEntry(mapping, 'KeyH', [r'р', r'Р', r'р', r'Р'], 'h'); + verifyEntry(mapping, 'KeyI', [r'ш', r'Ш', r'ш', r'Ш'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'о', r'О', r'о', r'О'], 'j'); + verifyEntry(mapping, 'KeyK', [r'л', r'Л', r'л', r'Л'], 'k'); + verifyEntry(mapping, 'KeyL', [r'д', r'Д', r'д', r'Д'], 'l'); + verifyEntry(mapping, 'KeyM', [r'ь', r'Ь', r'ь', r'Ь'], 'm'); + verifyEntry(mapping, 'KeyN', [r'т', r'Т', r'т', r'Т'], 'n'); + verifyEntry(mapping, 'KeyO', [r'щ', r'Щ', r'щ', r'Щ'], 'o'); + verifyEntry(mapping, 'KeyP', [r'з', r'З', r'з', r'З'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'й', r'Й', r'й', r'Й'], 'q'); + verifyEntry(mapping, 'KeyR', [r'к', r'К', r'к', r'К'], 'r'); + verifyEntry(mapping, 'KeyS', [r'ы', r'Ы', r'ы', r'Ы'], 's'); + verifyEntry(mapping, 'KeyT', [r'е', r'Е', r'е', r'Е'], 't'); + verifyEntry(mapping, 'KeyU', [r'г', r'Г', r'г', r'Г'], 'u'); + verifyEntry(mapping, 'KeyV', [r'м', r'М', r'м', r'М'], 'v'); + verifyEntry(mapping, 'KeyW', [r'ц', r'Ц', r'ц', r'Ц'], 'w'); + verifyEntry(mapping, 'KeyX', [r'ч', r'Ч', r'ч', r'Ч'], 'x'); + verifyEntry(mapping, 'KeyY', [r'н', r'Н', r'н', r'Н'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'я', r'Я', r'я', r'Я'], 'z'); + }); +} + +void testDarwin(LocaleKeymap mapping) { + group('de', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'∫', r'‹'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'™'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r'‰'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'Ï'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'Ì'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'ª', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'⁄', r'Û'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'º', r'ı'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'∆', r'ˆ'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'@', r'fl'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'˘'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'›'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'«', r'»'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'¸'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'‚', r'Í'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'˝'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'Á'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'Ù'], 'x'); + verifyEntry(mapping, 'KeyY', [r'z', r'Z', r'Ω', r'ˇ'], 'z'); + verifyEntry(mapping, 'KeyZ', [r'y', r'Y', r'¥', r'‡'], 'y'); + }); + + group('dvorak', () { + verifyEntry(mapping, 'Comma', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'x', r'X', r'≈', r'˛'], 'x'); + verifyEntry(mapping, 'KeyC', [r'j', r'J', r'∆', r'Ô'], 'j'); + verifyEntry(mapping, 'KeyD', [r'e', r'E', r'Dead', r'´'], 'e'); + verifyEntry(mapping, 'KeyF', [r'u', r'U', r'Dead', r'¨'], 'u'); + verifyEntry(mapping, 'KeyG', [r'i', r'I', r'Dead', r'ˆ'], 'i'); + verifyEntry(mapping, 'KeyH', [r'd', r'D', r'∂', r'Î'], 'd'); + verifyEntry(mapping, 'KeyI', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyJ', [r'h', r'H', r'˙', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyK', [r't', r'T', r'†', r'ˇ'], 't'); + verifyEntry(mapping, 'KeyL', [r'n', r'N', r'Dead', r'˜'], 'n'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'Â'], 'm'); + verifyEntry(mapping, 'KeyN', [r'b', r'B', r'∫', r'ı'], 'b'); + verifyEntry(mapping, 'KeyO', [r'r', r'R', r'®', r'‰'], 'r'); + verifyEntry(mapping, 'KeyP', [r'l', r'L', r'¬', r'Ò'], 'l'); + verifyEntry(mapping, 'KeyR', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyS', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyT', [r'y', r'Y', r'¥', r'Á'], 'y'); + verifyEntry(mapping, 'KeyU', [r'g', r'G', r'©', r'˝'], 'g'); + verifyEntry(mapping, 'KeyV', [r'k', r'K', r'˚', r''], 'k'); + verifyEntry(mapping, 'KeyX', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyY', [r'f', r'F', r'ƒ', r'Ï'], 'f'); + verifyEntry(mapping, 'Period', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'Semicolon', [r's', r'S', r'ß', r'Í'], 's'); + verifyEntry(mapping, 'Slash', [r'z', r'Z', r'Ω', r'¸'], 'z'); + }); + + group('en', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'∫', r'ı'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'Î'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Dead', r'´'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'Ï'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'˝'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'˙', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'ˆ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'∆', r'Ô'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'˚', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'¬', r'Ò'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'Â'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'˜'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'‰'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'Í'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'ˇ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'¨'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'˛'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r'Á'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Ω', r'¸'], 'z'); + }); + + group('en-ext', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'Dead', r'̄'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'Dead', r'̆'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'Dead', r'̧'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'ð', r'Ð'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Dead', r'́'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'Dead'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'Dead', r'̱'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'̛'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'Dead', r'̋'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'Dead', r'̊'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'Dead', r'̵'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'Dead', r'̨'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'̃'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'Dead', r'̦'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'‰'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'þ', r'Þ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'̈'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'Dead', r'̌'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'Dead', r'̇'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'Dead', r'̣'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Dead', r'̉'], 'z'); + }); + + group('en-intl', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'∫', r'ı'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'Î'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Dead', r'´'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'Ï'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'˝'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'˙', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'ˆ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'∆', r'Ô'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'˚', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'¬', r'Ò'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'Â'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'˜'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'‰'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'Í'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'ˇ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'¨'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'˛'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r'Á'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Ω', r'¸'], 'z'); + }); + + group('en-uk', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'∫', r'ı'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'Î'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Dead', r'‰'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'Ï'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'Ì'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'˙', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'È'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'∆', r'Ô'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'˚', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'¬', r'Ò'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'˜'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'ˆ'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'Â'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'Í'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'Ê'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'Ë'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'Ù'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r'Á'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Ω', r'Û'], 'z'); + }); + + group('es', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'ß', r''], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'©', r' '], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'∆'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r'€'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'fi'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'', r'fl'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'™', r' '], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r' ', r' '], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'¶', r'¯'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'§', r'ˇ'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r' ', r'˘'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'˚'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r' ', r'˙'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r' '], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'∫', r' '], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'‡'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r' ', r' '], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'æ', r'Æ'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'∑', r'›'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r' '], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Ω', r'‹'], 'z'); + }); + + group('fr', () { + verifyEntry(mapping, 'KeyA', [r'q', r'Q', r'‡', r'Ω'], 'q'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'ß', r'∫'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'©', r'¢'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'∆'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'ê', r'Ê'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'·'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'fi', r'fl'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'Ì', r'Î'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'î', r'ï'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'Ï', r'Í'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'È', r'Ë'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'¬', r'|'], 'l'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'ı'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'œ', r'Œ'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'a', r'A', r'æ', r'Æ'], 'a'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'‚'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'Ò', r'∑'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'™'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'º', r'ª'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'◊', r'√'], 'v'); + verifyEntry(mapping, 'KeyW', [r'z', r'Z', r'Â', r'Å'], 'z'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'⁄'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'Ú', r'Ÿ'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'w', r'W', r'‹', r'›'], 'w'); + verifyEntry(mapping, 'Semicolon', [r'm', r'M', r'µ', r'Ó'], 'm'); + }); + + group('it', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'∫', r'Í'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'©', r'Á'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'˘'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'€', r'È'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'˙'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'∞', r'˚'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'∆', r'¸'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'œ', r'Œ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'ª', r'˝'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'º', r'˛'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'¬', r'ˇ'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'Ú'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'Ó'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'„', r'‚'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'Ì'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'¯'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'™', r'Ò'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'Ù'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'É'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'Ω', r'À'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'†', r'‡'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'æ', r'Æ'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'∑', r' '], 'z'); + }); + + group('jp', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'∫', r'ı'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'Î'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Dead', r'´'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'Ï'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'˝'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'˙', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'ˆ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'∆', r'Ô'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'˚', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'¬', r'Ò'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'Â'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'˜'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'‰'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'Í'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'ˇ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'¨'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'˛'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r'Á'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Ω', r'¸'], 'z'); + }); + + group('jp-roman', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'Dead', r'̄'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'Dead', r'̆'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'Dead', r'̧'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'ð', r'Ð'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Dead', r'́'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r''], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'Dead'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'Dead', r'̱'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'̛'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'Dead', r'̋'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'Dead', r'̊'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'Dead', r'̵'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'Dead', r'̨'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'̃'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'Dead', r'̦'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'‰'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r''], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'þ', r'Þ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'̈'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'Dead', r'̌'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'Dead', r'̇'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'Dead', r'̣'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r''], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Dead', r'̉'], 'z'); + }); + + group('ko', () { + verifyEntry(mapping, 'KeyA', [r'ㅁ', r'ㅁ', r'a', r'A'], 'a'); + verifyEntry(mapping, 'KeyB', [r'ㅠ', r'ㅠ', r'b', r'B'], 'b'); + verifyEntry(mapping, 'KeyC', [r'ㅊ', r'ㅊ', r'c', r'C'], 'c'); + verifyEntry(mapping, 'KeyD', [r'ㅇ', r'ㅇ', r'd', r'D'], 'd'); + verifyEntry(mapping, 'KeyE', [r'ㄷ', r'ㄸ', r'e', r'E'], 'e'); + verifyEntry(mapping, 'KeyF', [r'ㄹ', r'ㄹ', r'f', r'F'], 'f'); + verifyEntry(mapping, 'KeyG', [r'ㅎ', r'ㅎ', r'g', r'G'], 'g'); + verifyEntry(mapping, 'KeyH', [r'ㅗ', r'ㅗ', r'h', r'H'], 'h'); + verifyEntry(mapping, 'KeyI', [r'ㅑ', r'ㅑ', r'i', r'I'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'ㅓ', r'ㅓ', r'j', r'J'], 'j'); + verifyEntry(mapping, 'KeyK', [r'ㅏ', r'ㅏ', r'k', r'K'], 'k'); + verifyEntry(mapping, 'KeyL', [r'ㅣ', r'ㅣ', r'l', r'L'], 'l'); + verifyEntry(mapping, 'KeyM', [r'ㅡ', r'ㅡ', r'm', r'M'], 'm'); + verifyEntry(mapping, 'KeyN', [r'ㅜ', r'ㅜ', r'n', r'N'], 'n'); + verifyEntry(mapping, 'KeyO', [r'ㅐ', r'ㅒ', r'o', r'O'], 'o'); + verifyEntry(mapping, 'KeyP', [r'ㅔ', r'ㅖ', r'p', r'P'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'ㅂ', r'ㅃ', r'q', r'Q'], 'q'); + verifyEntry(mapping, 'KeyR', [r'ㄱ', r'ㄲ', r'r', r'R'], 'r'); + verifyEntry(mapping, 'KeyS', [r'ㄴ', r'ㄴ', r's', r'S'], 's'); + verifyEntry(mapping, 'KeyT', [r'ㅅ', r'ㅆ', r't', r'T'], 't'); + verifyEntry(mapping, 'KeyU', [r'ㅕ', r'ㅕ', r'u', r'U'], 'u'); + verifyEntry(mapping, 'KeyV', [r'ㅍ', r'ㅍ', r'v', r'V'], 'v'); + verifyEntry(mapping, 'KeyW', [r'ㅈ', r'ㅉ', r'w', r'W'], 'w'); + verifyEntry(mapping, 'KeyX', [r'ㅌ', r'ㅌ', r'x', r'X'], 'x'); + verifyEntry(mapping, 'KeyY', [r'ㅛ', r'ㅛ', r'y', r'Y'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'ㅋ', r'ㅋ', r'z', r'Z'], 'z'); + }); + + group('pl', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'ą', r'Ą'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'ļ', r'ű'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ć', r'Ć'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'Ž'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'ę', r'Ę'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ń', r'ž'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'Ū'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'ķ', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'ť'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'∆', r'Ô'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'Ż', r'ū'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'ł', r'Ł'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'Ķ', r'ų'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'ń', r'Ń'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ó', r'Ó'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'Ļ', r'ł'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'Ō', r'ő'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'£'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ś', r'Ś'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'ś'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'Ť'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'ź', r'Ź'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'ī', r'Á'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'ż', r'Ż'], 'z'); + }); + + group('pt', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'∫', r'ı'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'Î'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Dead', r'´'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'Ï'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'˝'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'˙', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'ˆ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'∆', r'Ô'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'˚', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'¬', r'Ò'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'Â'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'˜'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'‰'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'Í'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'ˇ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'¨'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'˛'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r'Á'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Ω', r'¸'], 'z'); + }); + + group('ru', () { + verifyEntry(mapping, 'KeyA', [r'ф', r'Ф', r'ƒ', r'ƒ'], 'a'); + verifyEntry(mapping, 'KeyB', [r'и', r'И', r'и', r'И'], 'b'); + verifyEntry(mapping, 'KeyC', [r'с', r'С', r'≠', r'≠'], 'c'); + verifyEntry(mapping, 'KeyD', [r'в', r'В', r'ћ', r'Ћ'], 'd'); + verifyEntry(mapping, 'KeyE', [r'у', r'У', r'ќ', r'Ќ'], 'e'); + verifyEntry(mapping, 'KeyF', [r'а', r'А', r'÷', r'÷'], 'f'); + verifyEntry(mapping, 'KeyG', [r'п', r'П', r'©', r'©'], 'g'); + verifyEntry(mapping, 'KeyH', [r'р', r'Р', r'₽', r'₽'], 'h'); + verifyEntry(mapping, 'KeyI', [r'ш', r'Ш', r'ѕ', r'Ѕ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'о', r'О', r'°', r'•'], 'j'); + verifyEntry(mapping, 'KeyK', [r'л', r'Л', r'љ', r'Љ'], 'k'); + verifyEntry(mapping, 'KeyL', [r'д', r'Д', r'∆', r'∆'], 'l'); + verifyEntry(mapping, 'KeyM', [r'ь', r'Ь', r'~', r'~'], 'm'); + verifyEntry(mapping, 'KeyN', [r'т', r'Т', r'™', r'™'], 'n'); + verifyEntry(mapping, 'KeyO', [r'щ', r'Щ', r'ў', r'Ў'], 'o'); + verifyEntry(mapping, 'KeyP', [r'з', r'З', r'‘', r'’'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'й', r'Й', r'ј', r'Ј'], 'q'); + verifyEntry(mapping, 'KeyR', [r'к', r'К', r'®', r'®'], 'r'); + verifyEntry(mapping, 'KeyS', [r'ы', r'Ы', r'ы', r'Ы'], 's'); + verifyEntry(mapping, 'KeyT', [r'е', r'Е', r'†', r'†'], 't'); + verifyEntry(mapping, 'KeyU', [r'г', r'Г', r'ѓ', r'Ѓ'], 'u'); + verifyEntry(mapping, 'KeyV', [r'м', r'М', r'µ', r'µ'], 'v'); + verifyEntry(mapping, 'KeyW', [r'ц', r'Ц', r'џ', r'Џ'], 'w'); + verifyEntry(mapping, 'KeyX', [r'ч', r'Ч', r'≈', r'≈'], 'x'); + verifyEntry(mapping, 'KeyY', [r'н', r'Н', r'њ', r'Њ'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'я', r'Я', r'ђ', r'Ђ'], 'z'); + }); + + group('sv', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'', r'◊'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'›', r'»'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'∆'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'é', r'É'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'∫'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'¸', r'¯'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'˛', r'˘'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'ı', r'ˆ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'√', r'¬'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'ª', r'º'], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'fi', r'fl'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'’', r'”'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'‘', r'“'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'œ', r'Œ'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'•', r'°'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'√'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'∑'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'‡'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'ü', r'Ü'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'‹', r'«'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'Ω', r'˝'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'ˇ'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'µ', r'˜'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'÷', r'⁄'], 'z'); + }); + + group('zh-hans', () { + verifyEntry(mapping, 'KeyA', [r'a', r'A', r'å', r'Å'], 'a'); + verifyEntry(mapping, 'KeyB', [r'b', r'B', r'∫', r'ı'], 'b'); + verifyEntry(mapping, 'KeyC', [r'c', r'C', r'ç', r'Ç'], 'c'); + verifyEntry(mapping, 'KeyD', [r'd', r'D', r'∂', r'Î'], 'd'); + verifyEntry(mapping, 'KeyE', [r'e', r'E', r'Dead', r'´'], 'e'); + verifyEntry(mapping, 'KeyF', [r'f', r'F', r'ƒ', r'Ï'], 'f'); + verifyEntry(mapping, 'KeyG', [r'g', r'G', r'©', r'˝'], 'g'); + verifyEntry(mapping, 'KeyH', [r'h', r'H', r'˙', r'Ó'], 'h'); + verifyEntry(mapping, 'KeyI', [r'i', r'I', r'Dead', r'ˆ'], 'i'); + verifyEntry(mapping, 'KeyJ', [r'j', r'J', r'∆', r'Ô'], 'j'); + verifyEntry(mapping, 'KeyK', [r'k', r'K', r'˚', r''], 'k'); + verifyEntry(mapping, 'KeyL', [r'l', r'L', r'¬', r'Ò'], 'l'); + verifyEntry(mapping, 'KeyM', [r'm', r'M', r'µ', r'Â'], 'm'); + verifyEntry(mapping, 'KeyN', [r'n', r'N', r'Dead', r'˜'], 'n'); + verifyEntry(mapping, 'KeyO', [r'o', r'O', r'ø', r'Ø'], 'o'); + verifyEntry(mapping, 'KeyP', [r'p', r'P', r'π', r'∏'], 'p'); + verifyEntry(mapping, 'KeyQ', [r'q', r'Q', r'œ', r'Œ'], 'q'); + verifyEntry(mapping, 'KeyR', [r'r', r'R', r'®', r'‰'], 'r'); + verifyEntry(mapping, 'KeyS', [r's', r'S', r'ß', r'Í'], 's'); + verifyEntry(mapping, 'KeyT', [r't', r'T', r'†', r'ˇ'], 't'); + verifyEntry(mapping, 'KeyU', [r'u', r'U', r'Dead', r'¨'], 'u'); + verifyEntry(mapping, 'KeyV', [r'v', r'V', r'√', r'◊'], 'v'); + verifyEntry(mapping, 'KeyW', [r'w', r'W', r'∑', r'„'], 'w'); + verifyEntry(mapping, 'KeyX', [r'x', r'X', r'≈', r'˛'], 'x'); + verifyEntry(mapping, 'KeyY', [r'y', r'Y', r'¥', r'Á'], 'y'); + verifyEntry(mapping, 'KeyZ', [r'z', r'Z', r'Ω', r'¸'], 'z'); + }); +} diff --git a/third_party/web_locale_keymap/test/testing.dart b/third_party/web_locale_keymap/test/testing.dart new file mode 100644 index 0000000000000..a8e340f86b2f4 --- /dev/null +++ b/third_party/web_locale_keymap/test/testing.dart @@ -0,0 +1,55 @@ +//--------------------------------------------------------------------------------------------- +// Copyright (c) 2022 Google LLC +// Licensed under the MIT License. See License.txt in the project root for license information. +//--------------------------------------------------------------------------------------------*/ + +import 'package:test/test.dart'; +import 'package:web_locale_keymap/web_locale_keymap.dart'; + +final int _kLowerA = 'a'.codeUnitAt(0); +final int _kUpperA = 'A'.codeUnitAt(0); +final int _kLowerZ = 'z'.codeUnitAt(0); +final int _kUpperZ = 'Z'.codeUnitAt(0); + +bool _isLetter(String char) { + if (char.length != 1) { + return false; + } + final int charCode = char.codeUnitAt(0); + return (charCode >= _kLowerA && charCode <= _kLowerZ) + || (charCode >= _kUpperA && charCode <= _kUpperZ); +} + +String _fromCharCode(int? logicalKey) { + if (logicalKey == null) { + return ''; + } + return String.fromCharCode(logicalKey); +} + +void verifyEntry(LocaleKeymap mapping, String eventCode, List eventKeys, String mappedResult) { + // If the first two entry of KeyboardEvent.key are letter keys such as "a" and + // "A", then KeyboardEvent.keyCode is the upper letter such as "A". Otherwise, + // this field must not be used (in reality this field may or may not be + // platform independent). + int? eventKeyCode; + { + if (_isLetter(eventKeys[0]) && _isLetter(eventKeys[1])) { + eventKeyCode = eventKeys[0].toLowerCase().codeUnitAt(0); + } + } + + int index = 0; + for (final String eventKey in eventKeys) { + if (eventKey.isEmpty) { + continue; + } + test('$eventCode $index', () { + expect( + _fromCharCode(mapping.getLogicalKey(eventCode, eventKey, eventKeyCode ?? 0)), + mappedResult, + ); + }); + index += 1; + } +} diff --git a/tools/gen_web_locale_keymap/README.md b/tools/gen_web_locale_keymap/README.md new file mode 100644 index 0000000000000..d9ecaa17ee39c --- /dev/null +++ b/tools/gen_web_locale_keymap/README.md @@ -0,0 +1,66 @@ +# Web Locale Keymap Generator + +This script generates mapping data for `web_locale_keymap`. + +## Usage + +1. `cd` to this folder, and run `dart pub get`. +2. [Create a Github access token](https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token), then store it to environment variable `$GITHUB_TOKEN`. This token is only for quota controlling and does not need any scopes. +``` +# ~/.zshrc +export GITHUB_TOKEN= +``` +3. Run +``` +dart --enable-asserts bin/gen_web_locale_keymap.dart +``` + +### Help +For help on CLI, +``` +dart --enable-asserts bin/gen_web_locale_keymap.dart -h +``` + +## Explanation + +To derive a key map that allows international layout to properly trigger +shortcuts, we can't [simply map logical keys from the current +event](https://github.com/flutter/flutter/issues/100456). Instead, we need to +analyze the entire current layout and plan ahead. This algorithm, +which we call the benchmark planner, goes as follows: + +> Analyze every key of the current layout, +> 1. If a key can produce an alnum under some modifier, then this key is mapped to this alnum. +> 2. After the previous step, if some alnum is not mapped, they're mapped to their corresponding key on the US keyboard. +> 3. The remaining keys are mapped to the unicode plane according to their produced character. + +However, we can't simply apply this algorithm to Web: unlike other desktop +platforms, Web DOM API does not tell which keyboard layout the user is on, or +how the current layout maps keys (there is a KeyboardLayout API that is +supported only by Chrome, and explicitly refused by all other browsers). So we +have to invent a "blind" algorithm that applies to any layout, while keeping the +same result. + +Luckily, we're able to fetch a list of "all keyboard layouts" from +`Microsoft/VSCode` repo, and we analyzed all layouts beforehand, and managed to +combine the result into a huge `code -> key -> result` map. You would imagine it +being impossible, since different layouts might use the same `(code, key)` pair +for different characters, but in fact such conflicts are surprisingly few, and +all the conflicts are mapped to letters. For example, `es-linux` maps +`('KeyY', '←')` to `y`, while `de-linux` maps `('KeyY', '←')` to `z`. + +We can't distinguished these conflicts only by the `(code, key)` pair, but we +can use other information: `keyCode`. Now, keyCode is a deprecated property, but +we really don't see it being removed any time foreseeable. Also, although +keyCode is infamous for being platform-dependent, for letter keys it is always +equal to the letter character. Therefore such conflicting cases are all mapped +to a special value, `kUseKeyCode`, indicating "use keyCode". + +Moreover, to reduce the size of the map, we noticed there are certain patterns +that can be easily represented by some if statements. These patterns are +extracted as the so-called "heuristic mapper". This reduces the map from over +1600 entries to ~450 entries. + +To further reduce the package size overhead, the map is encoded into a string +that is decoded at run time. This reduces the package size over by 27% at the +cost of code complexity. diff --git a/tools/gen_web_locale_keymap/bin/gen_web_locale_keymap.dart b/tools/gen_web_locale_keymap/bin/gen_web_locale_keymap.dart new file mode 100644 index 0000000000000..a2f2bf3f4a8e3 --- /dev/null +++ b/tools/gen_web_locale_keymap/bin/gen_web_locale_keymap.dart @@ -0,0 +1,228 @@ +// Copyright 2014 The Flutter Authors. 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:io'; + +import 'package:args/args.dart'; +import 'package:gen_web_keyboard_layouts/benchmark_planner.dart'; +import 'package:gen_web_keyboard_layouts/common.dart'; +import 'package:gen_web_keyboard_layouts/github.dart'; +import 'package:gen_web_keyboard_layouts/layout_types.dart'; +import 'package:path/path.dart' as path; + +const String kEnvGithubToken = 'GITHUB_TOKEN'; + +String _renderTemplate(String template, Map dictionary) { + String result = template; + dictionary.forEach((String key, String value) { + final String localResult = result.replaceAll('@@@$key@@@', value); + if (localResult == result) { + print('Template key $key is not used.'); + } + result = localResult; + }); + return result; +} + +void _writeFileTo(String outputDir, String outputFileName, String body) { + final String outputPath = path.join(outputDir, outputFileName); + Directory(outputDir).createSync(recursive: true); + File(outputPath).writeAsStringSync(body); +} + +String _readSharedSegment(String path) { + const String kSegmentStartMark = '/*@@@ SHARED SEGMENT START @@@*/'; + const String kSegmentEndMark = '/*@@@ SHARED SEGMENT END @@@*/'; + final List lines = File(path).readAsStringSync().split('\n'); + // Defining the two variables as `late final` ensures that each mark is found + // once and only once, otherwise assertion errors will be thrown. + late final int startLine; + late final int endLine; + for (int lineNo = 0; lineNo < lines.length; lineNo += 1) { + if (lines[lineNo] == kSegmentStartMark) { + startLine = lineNo; + } else if (lines[lineNo] == kSegmentEndMark) { + endLine = lineNo; + } + } + assert(startLine < endLine); + return lines.sublist(startLine + 1, endLine).join('\n').trimRight(); +} + +typedef _ForEachAction = void Function(String key, V value); +void _sortedForEach(Map map, _ForEachAction action) { + map + .entries + .toList() + ..sort((MapEntry a, MapEntry b) => a.key.compareTo(b.key)) + ..forEach((MapEntry entry) { + action(entry.key, entry.value); + }); +} + +String _escapeStringToDart(String origin) { + // If there is no `'`, we can use the raw string surrounded by `'`. + if (!origin.contains("'")) { + return "r'$origin'"; + } else { + // If there is no `"`, we can use the raw string surrounded by `"`. + if (!origin.contains('"')) { + return 'r"$origin"'; + } else { + // If there are both kinds of quotes, we have to use non-raw string + // and escape necessary characters. + final String beforeQuote = origin + .replaceAll(r'\', r'\\') + .replaceAll(r'$', r'\$') + .replaceAll("'", r"\'"); + return "'$beforeQuote'"; + } + } +} + +typedef _ValueCompare = void Function(T?, T?, String path); +void _mapForEachEqual(Map a, Map b, _ValueCompare body, String path) { + assert(a.length == b.length, '$path.length: ${a.length} vs ${b.length}'); + for (final String key in a.keys) { + body(a[key], b[key], key); + } +} + +// Ensure that two maps are deeply equal. +// +// Differences will be thrown as assertion. +bool _verifyMap(Map> a, Map> b) { + _mapForEachEqual(a, b, (Map? aMap, Map? bMap, String path) { + _mapForEachEqual(aMap!, bMap!, (int? aValue, int? bValue, String path) { + assert(aValue == bValue && aValue != null, '$path: $aValue vs $bValue'); + }, path); + }, ''); + return true; +} + +String _buildMapString(Iterable layouts) { + final Map> originalMap = combineLayouts(layouts); + final List compressed = marshallMappingData(originalMap); + final Map> uncompressed = unmarshallMappingData(compressed.join()); + assert(_verifyMap(originalMap, uncompressed)); + return ' return unmarshallMappingData(\n' + '${compressed.map((String line) => ' ${_escapeStringToDart(line)}\n').join()}' + ' ); // ${compressed.join().length} characters'; +} + +String _buildTestCasesString(List layouts) { + final List layoutsString = []; + for (final Layout layout in layouts) { + final List layoutEntries = []; + _sortedForEach(planLayout(layout.entries), (String eventCode, int logicalKey) { + final LayoutEntry entry = layout.entries[eventCode]!; + layoutEntries.add(" verifyEntry(mapping, '$eventCode', [" + '${entry.printables.map(_escapeStringToDart).join(', ')}' + "], '${String.fromCharCode(logicalKey)}');"); + }); + layoutsString.add(''' + group('${layout.language}', () { +${layoutEntries.join('\n')} + }); +'''); + } + return layoutsString.join('\n').trimRight(); +} + +Future main(List rawArguments) async { + final Map env = Platform.environment; + final ArgParser argParser = ArgParser(); + argParser.addFlag( + 'force', + abbr: 'f', + negatable: false, + help: 'Make a new request to GitHub even if a cache is detected', + ); + argParser.addFlag( + 'help', + abbr: 'h', + negatable: false, + help: 'Print help for this command.', + ); + + final ArgResults parsedArguments = argParser.parse(rawArguments); + + if (parsedArguments['help'] as bool) { + print(argParser.usage); + exit(0); + } + + bool enabledAssert = false; + assert(() { + enabledAssert = true; + return true; + }()); + if (!enabledAssert) { + print('Error: This script must be run with assert enabled. Please rerun with --enable-asserts.'); + exit(1); + } + + final String? envGithubToken = env[kEnvGithubToken]; + if (envGithubToken == null) { + print('Error: Environment variable $kEnvGithubToken not found.\n\n' + 'Set the environment variable $kEnvGithubToken as a GitHub personal access\n' + 'token for authentication. This token is only used for quota controlling\n' + 'and does not need any scopes. Create one at\n' + 'https://docs.github.com/en/authentication/keeping-your-account-and-data-secure/creating-a-personal-access-token.', + ); + exit(1); + } + + // The root of this package. The folder that is called + // 'gen_web_locale_keymap' and contains 'pubspec.yaml'. + final Directory packageRoot = Directory(path.dirname(Platform.script.toFilePath())).parent; + + // The root of the output package. The folder that is called + // 'web_locale_keymap' and contains 'pubspec.yaml'. + final String outputRoot = path.join(packageRoot.parent.parent.path, + 'third_party', 'web_locale_keymap'); + + final GithubResult githubResult = await fetchFromGithub( + githubToken: envGithubToken, + force: parsedArguments['force'] as bool, + cacheRoot: path.join(packageRoot.path, '.cache'), + ); + + final List winLayouts = githubResult.layouts.where((Layout layout) => + layout.platform == LayoutPlatform.win).toList(); + final List linuxLayouts = githubResult.layouts.where((Layout layout) => + layout.platform == LayoutPlatform.linux).toList(); + final List darwinLayouts = githubResult.layouts.where((Layout layout) => + layout.platform == LayoutPlatform.darwin).toList(); + + // Generate the definition file. + _writeFileTo( + path.join(outputRoot, 'lib', 'web_locale_keymap'), + 'key_mappings.g.dart', + _renderTemplate( + File(path.join(packageRoot.path, 'data', 'key_mappings.dart.tmpl')).readAsStringSync(), + { + 'COMMIT_URL': githubResult.url, + 'WIN_MAPPING': _buildMapString(winLayouts), + 'LINUX_MAPPING': _buildMapString(linuxLayouts), + 'DARWIN_MAPPING': _buildMapString(darwinLayouts), + 'COMMON': _readSharedSegment(path.join(packageRoot.path, 'lib', 'common.dart')), + }, + ), + ); + + // Generate the test cases. + _writeFileTo( + path.join(outputRoot, 'test'), + 'test_cases.g.dart', + _renderTemplate( + File(path.join(packageRoot.path, 'data', 'test_cases.dart.tmpl')).readAsStringSync(), + { + 'WIN_CASES': _buildTestCasesString(winLayouts), + 'LINUX_CASES': _buildTestCasesString(linuxLayouts), + 'DARWIN_CASES': _buildTestCasesString(darwinLayouts), + }, + ), + ); +} diff --git a/tools/gen_web_locale_keymap/data/key_mappings.dart.tmpl b/tools/gen_web_locale_keymap/data/key_mappings.dart.tmpl new file mode 100644 index 0000000000000..fa54363de1f9f --- /dev/null +++ b/tools/gen_web_locale_keymap/data/key_mappings.dart.tmpl @@ -0,0 +1,50 @@ +//--------------------------------------------------------------------------------------------- +// Copyright (c) 2022 Google LLC +// Licensed under the MIT License. See License.txt in the project root for license information. +//--------------------------------------------------------------------------------------------*/ + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// +// This file is auto generated by flutter/engine:flutter/tools/gen_web_keyboard_layouts based on +// @@@COMMIT_URL@@@ +// +// Edit the following files instead: +// +// - Script: lib/main.dart +// - Templates: data/*.tmpl +// +// See flutter/engine:flutter/tools/gen_web_keyboard_layouts/README.md for more information. +@@@COMMON@@@ + +/// Data for [LocaleKeymap] on Windows. +/// +/// Do not use this value, but [LocaleKeymap.win] instead. +/// +/// The keys are `KeyboardEvent.code` and then `KeyboardEvent.key`. The values +/// are logical keys or [kUseKeyCode]. Entries that can be derived using +/// heuristics have been omitted. +Map> getMappingDataWin() { +@@@WIN_MAPPING@@@ +} + +/// Data for [LocaleKeymap] on Linux. +/// +/// Do not use this value, but [LocaleKeymap.linux] instead. +/// +/// The keys are `KeyboardEvent.code` and then `KeyboardEvent.key`. The values +/// are logical keys or [kUseKeyCode]. Entries that can be derived using +/// heuristics have been omitted. +Map> getMappingDataLinux() { +@@@LINUX_MAPPING@@@ +} + +/// Data for [LocaleKeymap] on Darwin. +/// +/// Do not use this value, but [LocaleKeymap.darwin] instead. +/// +/// The keys are `KeyboardEvent.code` and then `KeyboardEvent.key`. The values +/// are logical keys or [kUseKeyCode]. Entries that can be derived using +/// heuristics have been omitted. +Map> getMappingDataDarwin() { +@@@DARWIN_MAPPING@@@ +} diff --git a/tools/gen_web_locale_keymap/data/test_cases.dart.tmpl b/tools/gen_web_locale_keymap/data/test_cases.dart.tmpl new file mode 100644 index 0000000000000..41a94b0a68173 --- /dev/null +++ b/tools/gen_web_locale_keymap/data/test_cases.dart.tmpl @@ -0,0 +1,32 @@ +//--------------------------------------------------------------------------------------------- +// Copyright (c) 2022 Google LLC +// Licensed under the MIT License. See License.txt in the project root for license information. +//--------------------------------------------------------------------------------------------*/ + +// DO NOT EDIT -- DO NOT EDIT -- DO NOT EDIT +// +// This file is auto generated by flutter/engine:flutter/tools/gen_web_keyboard_layouts based on +// https://github.com/microsoft/vscode/tree/@@@COMMIT_ID@@@/src/vs/workbench/services/keybinding/browser/keyboardLayouts +// +// Edit the following files instead: +// +// - Script: lib/main.dart +// - Templates: data/*.tmpl +// +// See flutter/engine:flutter/tools/gen_web_keyboard_layouts/README.md for more information. + +import 'package:test/test.dart'; +import 'package:web_locale_keymap/web_locale_keymap.dart'; +import 'testing.dart'; + +void testWin(LocaleKeymap mapping) { +@@@WIN_CASES@@@ +} + +void testLinux(LocaleKeymap mapping) { +@@@LINUX_CASES@@@ +} + +void testDarwin(LocaleKeymap mapping) { +@@@DARWIN_CASES@@@ +} diff --git a/tools/gen_web_locale_keymap/lib/benchmark_planner.dart b/tools/gen_web_locale_keymap/lib/benchmark_planner.dart new file mode 100644 index 0000000000000..9498406381eb2 --- /dev/null +++ b/tools/gen_web_locale_keymap/lib/benchmark_planner.dart @@ -0,0 +1,104 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'common.dart'; +import 'layout_types.dart'; + +// Maps all mandatory goals from the character to eventScanCode. +// +// Mandatory goals are all the alnum keys. These keys must be assigned at the +// end of layout planning. +final Map _kMandatoryGoalsByChar = Map.fromEntries( + kLayoutGoals + .entries + .where((MapEntry entry) => isLetter(entry.value.codeUnitAt(0))) + .map((MapEntry entry) => MapEntry(entry.value!, entry.key)) +); + +/// Plan a layout into a map from eventCode to logical key. +/// +/// If a eventCode does not exist in this map, then this event's logical key +/// should be derived on the fly. +/// +/// This function defines the benchmark planning algorithm if there were a way +/// to know the current keyboard layout. +Map planLayout(Map entries) { + // The logical key is derived in the following rules: + // + // 1. If any clue (the four possible printables) of the key is a mandatory + // goal (alnum), then the goal is the logical key. + // 2. If a mandatory goal is not assigned in the way of #1, then it is + // assigned to the physical key as mapped in the US layout. + // 3. Derived on the fly from key, code, and keyCode. + // + // The map returned from this function contains the first two rules. + + // Unresolved mandatory goals, mapped from printables to KeyboardEvent.code. + // This map will be modified during this function and thus is a clone. + final Map mandatoryGoalsByChar = {..._kMandatoryGoalsByChar}; + // The result mapping from KeyboardEvent.code to logical key. + final Map result = {}; + + entries.forEach((String eventCode, LayoutEntry entry) { + for (final String printable in entry.printables) { + if (mandatoryGoalsByChar.containsKey(printable)) { + result[eventCode] = printable.codeUnitAt(0); + mandatoryGoalsByChar.remove(printable); + break; + } + } + }); + + // Ensure all mandatory goals are assigned. + mandatoryGoalsByChar.forEach((String character, String code) { + assert(!result.containsKey(code), 'Code $code conflicts.'); + result[code] = character.codeUnitAt(0); + }); + return result; +} + +bool _isLetterOrMappedToKeyCode(int charCode) { + return isLetterChar(charCode) || charCode == kUseKeyCode; +} + +/// Plan all layouts, and summarize them into a huge table of EventCode -> +/// EventKey -> logicalKey. +/// +/// The resulting logicalKey can also be kUseKeyCode. +/// +/// If a eventCode does not exist in this map, then this event's logical key +/// should be derived on the fly. +/// +/// Entries that can be derived using heuristics are omitted. +Map> combineLayouts(Iterable layouts) { + final Map> result = >{}; + for (final Layout layout in layouts) { + planLayout(layout.entries).forEach((String eventCode, int logicalKey) { + final Map codeMap = result.putIfAbsent(eventCode, () => {}); + final LayoutEntry entry = layout.entries[eventCode]!; + for (final String eventKey in entry.printables) { + if (eventKey.isEmpty) { + continue; + } + // Found conflict. Assert that all such cases can be solved with + // keyCode. + if (codeMap.containsKey(eventKey) && codeMap[eventKey] != logicalKey) { + assert(isLetterChar(logicalKey)); + assert(_isLetterOrMappedToKeyCode(codeMap[eventKey]!), '$eventCode, $eventKey, ${codeMap[eventKey]!}'); + codeMap[eventKey] = kUseKeyCode; + } else { + codeMap[eventKey] = logicalKey; + } + } + }); + } + // Remove mapping results that can be derived using heuristics. + result.removeWhere((String eventCode, Map codeMap) { + codeMap.removeWhere((String eventKey, int logicalKey) => + heuristicMapper(eventCode, eventKey) == logicalKey, + ); + return codeMap.isEmpty; + }); + return result; +} diff --git a/tools/gen_web_locale_keymap/lib/common.dart b/tools/gen_web_locale_keymap/lib/common.dart new file mode 100644 index 0000000000000..2d8526d8773e3 --- /dev/null +++ b/tools/gen_web_locale_keymap/lib/common.dart @@ -0,0 +1,262 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +// The following segment is not only used in the generating script, but also +// copied to the generated package. +/*@@@ SHARED SEGMENT START @@@*/ + +/// Used in the final mapping indicating the logical key should be derived from +/// KeyboardEvent.keyCode. +/// +/// This value is chosen because it's a printable character within EASCII that +/// will never be mapped to (checked in the marshalling algorithm). +const int kUseKeyCode = 0xFF; + +/// Used in the final mapping indicating the event key is 'Dead', the dead key. +final String _kUseDead = String.fromCharCode(0xFE); + +/// The KeyboardEvent.key for a dead key. +const String _kEventKeyDead = 'Dead'; + +/// A map of all goals from the scan codes to their mapped value in US layout. +const Map kLayoutGoals = { + 'KeyA': 'a', + 'KeyB': 'b', + 'KeyC': 'c', + 'KeyD': 'd', + 'KeyE': 'e', + 'KeyF': 'f', + 'KeyG': 'g', + 'KeyH': 'h', + 'KeyI': 'i', + 'KeyJ': 'j', + 'KeyK': 'k', + 'KeyL': 'l', + 'KeyM': 'm', + 'KeyN': 'n', + 'KeyO': 'o', + 'KeyP': 'p', + 'KeyQ': 'q', + 'KeyR': 'r', + 'KeyS': 's', + 'KeyT': 't', + 'KeyU': 'u', + 'KeyV': 'v', + 'KeyW': 'w', + 'KeyX': 'x', + 'KeyY': 'y', + 'KeyZ': 'z', + 'Digit1': '1', + 'Digit2': '2', + 'Digit3': '3', + 'Digit4': '4', + 'Digit5': '5', + 'Digit6': '6', + 'Digit7': '7', + 'Digit8': '8', + 'Digit9': '9', + 'Digit0': '0', + 'Minus': '-', + 'Equal': '=', + 'BracketLeft': '[', + 'BracketRight': ']', + 'Backslash': r'\', + 'Semicolon': ';', + 'Quote': "'", + 'Backquote': '`', + 'Comma': ',', + 'Period': '.', + 'Slash': '/', +}; + +final int _kLowerA = 'a'.codeUnitAt(0); +final int _kUpperA = 'A'.codeUnitAt(0); +final int _kLowerZ = 'z'.codeUnitAt(0); +final int _kUpperZ = 'Z'.codeUnitAt(0); + +bool _isAscii(int charCode) { + // 0x20 is the first printable character in ASCII. + return charCode >= 0x20 && charCode <= 0x7F; +} + +/// Returns whether the `char` is a single character of a letter or a digit. +bool isLetter(int charCode) { + return (charCode >= _kLowerA && charCode <= _kLowerZ) + || (charCode >= _kUpperA && charCode <= _kUpperZ); +} + +/// A set of rules that can derive a large number of logical keys simply from +/// the event's code and key. +/// +/// This greatly reduces the entries needed in the final mapping. +int? heuristicMapper(String code, String key) { + // Digit code: return the digit by event code. + if (code.startsWith('Digit')) { + assert(code.length == 6); + return code.codeUnitAt(5); // The character immediately after 'Digit' + } + final int charCode = key.codeUnitAt(0); + // Non-ascii: return the goal (i.e. US mapping by event code). + if (key.length > 1 || !_isAscii(charCode)) { + return kLayoutGoals[code]?.codeUnitAt(0); + } + // Letter key: return the event key letter. + if (isLetter(charCode)) { + return key.toLowerCase().codeUnitAt(0); + } + return null; +} + +// Maps an integer to a printable EASCII character by adding it to this value. +// +// We could've chosen 0x20, the first printable character, for a slightly bigger +// range, but it's prettier this way and sufficient. +final int _kMarshallIntBase = '0'.codeUnitAt(0); + +class _StringStream { + _StringStream(this._data) : _offset = 0; + + final String _data; + final Map _goalToEventCode = Map.fromEntries( + kLayoutGoals + .entries + .map((MapEntry beforeEntry) => + MapEntry(beforeEntry.value.codeUnitAt(0), beforeEntry.key)) + ); + + int get offest => _offset; + int _offset; + + int readIntAsVerbatim() { + final int result = _data.codeUnitAt(_offset); + _offset += 1; + assert(result >= _kMarshallIntBase); + return result - _kMarshallIntBase; + } + + int readIntAsChar() { + final int result = _data.codeUnitAt(_offset); + _offset += 1; + return result; + } + + String readEventKey() { + final String char = String.fromCharCode(readIntAsChar()); + if (char == _kUseDead) { + return _kEventKeyDead; + } else { + return char; + } + } + + String readEventCode() { + final int charCode = _data.codeUnitAt(_offset); + _offset += 1; + return _goalToEventCode[charCode]!; + } +} + +Map _unmarshallCodeMap(_StringStream stream) { + final int entryNum = stream.readIntAsVerbatim(); + return Map.fromEntries((() sync* { + for (int entryIndex = 0; entryIndex < entryNum; entryIndex += 1) { + yield MapEntry(stream.readEventKey(), stream.readIntAsChar()); + } + })()); +} + +/// Decode a key mapping data out of the string. +Map> unmarshallMappingData(String compressed) { + final _StringStream stream = _StringStream(compressed); + final int eventCodeNum = stream.readIntAsVerbatim(); + return Map>.fromEntries((() sync* { + for (int eventCodeIndex = 0; eventCodeIndex < eventCodeNum; eventCodeIndex += 1) { + yield MapEntry>(stream.readEventCode(), _unmarshallCodeMap(stream)); + } + })()); +} + +/*@@@ SHARED SEGMENT END @@@*/ + +/// Whether the given charCode is a ASCII letter. +bool isLetterChar(int charCode) { + return (charCode >= _kLowerA && charCode <= _kLowerZ) + || (charCode >= _kUpperA && charCode <= _kUpperZ); +} + +bool _isPrintableEascii(int charCode) { + return charCode >= 0x20 && charCode <= 0xFF; +} + +typedef _ForEachAction = void Function(String key, V value); +void _sortedForEach(Map map, _ForEachAction action) { + map + .entries + .toList() + ..sort((MapEntry a, MapEntry b) => a.key.compareTo(b.key)) + ..forEach((MapEntry entry) { + action(entry.key, entry.value); + }); +} + +// Encode a small integer as a character by its value. +// +// For example, 0x48 is encoded as '0'. This means that values within 0x0 - 0x19 +// or greater than 0xFF are forbidden. +void _marshallIntAsChar(StringBuffer builder, int value) { + assert(_isPrintableEascii(value), '$value'); + builder.writeCharCode(value); +} + +const int _kMarshallIntEnd = 0xFF; // The last printable EASCII. +// Encode a small integer as a character based on a certain printable codepoint. +// +// For example, 0x0 is encoded as '0', and 0x1 is encoded as '1'. This function +// allows smaller values than _marshallIntAsChar. +void _marshallIntAsVerbatim(StringBuffer builder, int value) { + final int result = value + _kMarshallIntBase; + assert(result <= _kMarshallIntEnd); + builder.writeCharCode(result); +} + +void _marshallEventCode(StringBuffer builder, String value) { + // Instead of recording the entire eventCode, since the eventCode is mapped + // 1-to-1 to a character in kLayoutGoals, we record the goal instead. + final String char = kLayoutGoals[value]!; + builder.write(char); +} + +void _marshallEventKey(StringBuffer builder, String value) { + if (value == _kEventKeyDead) { + builder.write(_kUseDead); + } else { + assert(value.length == 1, value); + assert(value != _kUseDead); + builder.write(value); + } +} + +/// Encode a key mapping data into a list of strings. +/// +/// The list of strings should be used concatenated, but is returned this way +/// for aesthetic purposes (one entry per line). +/// +/// The algorithm aims at encoding the map directly into a printable string +/// (instead of a binary stream converted by base64). Some characters in the +/// string can be multi-byte, which means the decoder should parse the string +/// using substr instead of as a binary stream. +List marshallMappingData(Map> mappingData) { + final StringBuffer builder = StringBuffer(); + _marshallIntAsVerbatim(builder, mappingData.length); + _sortedForEach(mappingData, (String eventCode, Map codeMap) { + builder.write('\n'); + _marshallEventCode(builder, eventCode); + _marshallIntAsVerbatim(builder, codeMap.length); + _sortedForEach(codeMap, (String eventKey, int logicalKey) { + _marshallEventKey(builder, eventKey); + _marshallIntAsChar(builder, logicalKey); + }); + }); + return builder.toString().split('\n'); +} diff --git a/tools/gen_web_locale_keymap/lib/github.dart b/tools/gen_web_locale_keymap/lib/github.dart new file mode 100644 index 0000000000000..9a3f6866e03e9 --- /dev/null +++ b/tools/gen_web_locale_keymap/lib/github.dart @@ -0,0 +1,319 @@ +// Copyright 2014 The Flutter Authors. 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:io'; +import 'package:http/http.dart' as http; +import 'package:meta/meta.dart' show immutable; +import 'package:path/path.dart' as path; + +import 'common.dart'; +import 'json_get.dart'; +import 'layout_types.dart'; + +/// Signature for function that asynchonously returns a value. +typedef AsyncGetter = Future Function(); + +/// The filename of the local cache for the GraphQL response. +const String _githubCacheFileName = 'github-response.json'; + +/// The file of the remote repo to query. +const String _githubTargetFolder = 'src/vs/workbench/services/keybinding/browser/keyboardLayouts'; + +/// The full query string for GraphQL. +const String _githubQuery = ''' +{ + repository(owner: "microsoft", name: "vscode") { + defaultBranchRef { + target { + ... on Commit { + history(first: 1) { + nodes { + oid + file(path: "$_githubTargetFolder") { + extension lineCount object { + ... on Tree { + entries { + name object { + ... on Blob { + text + } + } + } + } + } + } + } + } + } + } + } + } +} +'''; + +/// All goals in the form of KeyboardEvent.key. +final List _kGoalKeys = kLayoutGoals.keys.toList(); + +/// A map from the key of `kLayoutGoals` (KeyboardEvent.key) to an +/// auto-incremental index. +final Map _kGoalToIndex = Map.fromEntries( + _kGoalKeys.asMap().entries.map( + (MapEntry entry) => MapEntry(entry.value, entry.key)), +); + +/// Retrieve a string using the procedure defined by `ifNotExist` based on the +/// cache file at `cachePath`. +/// +/// If `forceRefresh` is false, this function tries to read the cache file, calls +/// `ifNotExist` when necessary, and writes the result to the cache. +/// +/// If `forceRefresh` is true, this function never read the cache file, always +/// calls `ifNotExist` when necessary, and still writes the result to the cache. +/// +/// Exceptions from `ifNotExist` will be thrown, while exceptions related to +/// caching are only printed. +Future _tryCached(String cachePath, bool forceRefresh, AsyncGetter ifNotExist) async { + final File cacheFile = File(cachePath); + if (!forceRefresh && cacheFile.existsSync()) { + try { + final String result = cacheFile.readAsStringSync(); + print('Using GitHub cache.'); + return result; + } catch (exception) { + print('Error reading GitHub cache, rebuilding. Details: $exception'); + } + } + final String result = await ifNotExist(); + IOSink? sink; + try { + print('Requesting from GitHub...'); + Directory(path.dirname(cachePath)).createSync(recursive: true); + sink = cacheFile.openWrite(); + cacheFile.writeAsStringSync(result); + } catch (exception) { + print('Error writing GitHub cache. Details: $exception'); + } finally { + sink?.close(); + } + return result; +} + +/// Make a GraphQL request, cache it, and return the result. +/// +/// If `forceRefresh` is false, this function tries to read the cache file at +/// `cachePath`. Regardless of `forceRefresh`, the response is always recorded +/// in the cache file. +Future> _fetchGithub(String githubToken, bool forceRefresh, String cachePath) async { + final String response = await _tryCached(cachePath, forceRefresh, () async { + final String condensedQuery = _githubQuery + .replaceAll(RegExp(r'\{ +'), '{') + .replaceAll(RegExp(r' +\}'), '}'); + final http.Response response = await http.post( + Uri.parse('https://api.github.com/graphql'), + headers: { + 'Content-Type': 'application/json; charset=UTF-8', + 'Authorization': 'bearer $githubToken', + }, + body: jsonEncode({ + 'query': condensedQuery, + }), + ); + if (response.statusCode != 200) { + throw Exception('Request to GitHub failed with status code ${response.statusCode}: ${response.reasonPhrase}'); + } + return response.body; + }); + return jsonDecode(response) as Map; +} + +@immutable +class _GitHubFile { + const _GitHubFile({required this.name, required this.content}); + + final String name; + final String content; +} + +_GitHubFile _jsonGetGithubFile(JsonContext files, int index) { + final JsonContext file = jsonGetIndex(files, index); + return _GitHubFile( + name: jsonGetKey(file, 'name').current, + content: jsonGetPath(file, 'object.text').current, + ); +} + +/// Parses a literal JavaScript string that represents a character, which might +/// have been escaped or empty. +String _parsePrintable(String rawString, int isDeadKey) { + // Parse a char represented in unicode hex, such as \u001b. + final RegExp hexParser = RegExp(r'^\\u([0-9a-fA-F]+)$'); + + if (isDeadKey != 0) { + return LayoutEntry.kDeadKey; + } + if (rawString.isEmpty) { + return ''; + } + final RegExpMatch? hexMatch = hexParser.firstMatch(rawString); + if (hexMatch != null) { + final int codeUnit = int.parse(hexMatch.group(1)!, radix: 16); + return String.fromCharCode(codeUnit); + } + return const { + r'\\': r'\', + r'\r': '\r', + r'\b': '\b', + r'\t': '\t', + r"\'": "'", + }[rawString] ?? rawString; +} + +LayoutPlatform _platformFromGithubString(String origin) { + switch (origin) { + case 'win': + return LayoutPlatform.win; + case 'linux': + return LayoutPlatform.linux; + case 'darwin': + return LayoutPlatform.darwin; + default: + throw ArgumentError('Unexpected platform "$origin".'); + } +} + +/// Parses a single layout file. +Layout _parseLayoutFromGithubFile(_GitHubFile file) { + final Map entries = {}; + + // Parse a line that looks like the following, and get its key as well as + // the content within the square bracket. + // + // F19: [], + // KeyZ: ['y', 'Y', '', '', 0, 'VK_Y'], + final RegExp lineParser = RegExp(r'^[ \t]*(.+?): \[(.*)\],$'); + // Parse each child of the content within the square bracket. + final RegExp listParser = RegExp(r"^'(.*?)', '(.*?)', '(.*?)', '(.*?)', (\d)(?:, '(.+)')?$"); + file.content.split('\n').forEach((String line) { + final RegExpMatch? lineMatch = lineParser.firstMatch(line); + if (lineMatch == null) { + return; + } + // KeyboardKey.code, such as "KeyZ". + final String eventCode = lineMatch.group(1)!; + // Only record goals. + if (!_kGoalToIndex.containsKey(eventCode)) { + return; + } + + // Comma-separated definition as a string, such as "'y', 'Y', '', '', 0, 'VK_Y'". + final String definition = lineMatch.group(2)!; + if (definition.isEmpty) { + return; + } + // Group 1-4 are single strings for an entry, such as "y", "", "\u001b". + // Group 5 is the dead mask. + final RegExpMatch? listMatch = listParser.firstMatch(definition); + assert(listMatch != null, 'Unable to match $definition'); + final int deadMask = int.parse(listMatch!.group(5)!, radix: 10); + + entries[eventCode] = LayoutEntry( + [ + _parsePrintable(listMatch.group(1)!, deadMask & 0x1), + _parsePrintable(listMatch.group(2)!, deadMask & 0x2), + _parsePrintable(listMatch.group(3)!, deadMask & 0x4), + _parsePrintable(listMatch.group(4)!, deadMask & 0x8), + ], + ); + }); + + for (final String goalKey in _kGoalKeys) { + entries.putIfAbsent(goalKey, () => LayoutEntry.empty); + } + + // Parse the file name, which looks like "en-belgian.win.ts". + final RegExp fileNameParser = RegExp(r'^([^.]+)\.([^.]+)\.ts$'); + late final Layout layout; + try { + final RegExpMatch? match = fileNameParser.firstMatch(file.name); + final String layoutName = match!.group(1)!; + final LayoutPlatform platform = _platformFromGithubString(match.group(2)!); + layout = Layout(layoutName, platform, entries); + } catch (exception) { + throw ArgumentError('Unrecognizable file name ${file.name}.'); + } + return layout; +} + +/// Sort layouts by language first, then by platform. +int _sortLayout(Layout a, Layout b) { + int result = a.language.compareTo(b.language); + if (result == 0) { + result = a.platform.index.compareTo(b.platform.index); + } + return result; +} + +/// The overall results returned from the GitHub request. +class GithubResult { + /// Create a [GithubResult]. + const GithubResult(this.layouts, this.url); + + /// All layouts, sorted. + final List layouts; + + /// The URL that points to the source folder of the VSCode GitHub repo, + /// containing the correct commit hash. + final String url; +} + +/// Fetch necessary information from the VSCode GitHub repo. +/// +/// The GraphQL request is made using the token `githubToken` (which requires +/// no extra access). The response is cached in files under directory +/// `cacheRoot`. +/// +/// If `force` is false, this function tries to read the cache. Regardless of +/// `force`, the response is always recorded in the cache. +Future fetchFromGithub({ + required String githubToken, + required bool force, + required String cacheRoot, +}) async { + // Fetch files from GitHub. + final Map githubBody = await _fetchGithub( + githubToken, + force, + path.join(cacheRoot, _githubCacheFileName), + ); + + // Parse the result from GitHub. + final JsonContext commitJson = jsonGetPath( + JsonContext.root(githubBody), + 'data.repository.defaultBranchRef.target.history.nodes.0', + ); + final String commitId = jsonGetKey(commitJson, 'oid').current; + final JsonContext fileListJson = jsonGetPath( + commitJson, + 'file.object.entries', + ); + final Iterable<_GitHubFile> files = Iterable<_GitHubFile>.generate( + fileListJson.current.length, + (int index) => _jsonGetGithubFile(fileListJson, index), + ).where( + // Exclude controlling files, which contain no layout information. + (_GitHubFile file) => !file.name.startsWith('layout.contribution.') + && !file.name.startsWith('_.contribution'), + ); + + // Layouts must be sorted to ensure that the output file has a fixed order. + final List layouts = files.map(_parseLayoutFromGithubFile) + .toList() + ..sort(_sortLayout); + + final String url = 'https://github.com/microsoft/vscode/tree/$commitId/$_githubTargetFolder'; + return GithubResult(layouts, url); + +// +} diff --git a/tools/gen_web_locale_keymap/lib/json_get.dart b/tools/gen_web_locale_keymap/lib/json_get.dart new file mode 100644 index 0000000000000..18406ad8c7d31 --- /dev/null +++ b/tools/gen_web_locale_keymap/lib/json_get.dart @@ -0,0 +1,106 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +import 'package:meta/meta.dart' show immutable; + +/// A subtree of a JSON object as well as its path. +@immutable +class JsonContext { + /// Create a [JsonContext] that represents a subtree. + const JsonContext(this.current, this.path); + + /// The content of the subtree. + final T current; + /// The path from the root. + final List path; + + /// Create a [JsonContext] that represents the root of a tree. + static JsonContext> root(Map root) { + return JsonContext>(root, const []); + } +} + +/// A JSON object. +typedef JsonObject = Map; + +/// A JSON array. +typedef JsonArray = List; + +String _jsonTypeErrorMessage(List currentPath, String nextKey, Type expectedType, Type actualType) { + return 'Unexpected value at path ${currentPath.join('.')}.$nextKey: ' + 'Expects $expectedType but got $actualType.'; +} + +/// Returns a JSON object's specified key. +/// +/// If the result is not of type `T`, throws an `ArgumentError`. +JsonContext jsonGetKey(JsonContext context, String key) { + final dynamic result = context.current[key]; + if (result is! T) { + throw ArgumentError(_jsonTypeErrorMessage(context.path, key, T, result.runtimeType)); + } + return JsonContext(result, [...context.path, key]); +} + +/// Returns a JSON array's specified index. +/// +/// If the subtree is not of type `T`, throws an `ArgumentError`. +JsonContext jsonGetIndex(JsonContext context, int index) { + final dynamic result = context.current[index]; + if (result is! T) { + throw ArgumentError(_jsonTypeErrorMessage(context.path, '$index', T, result.runtimeType)); + } + return JsonContext(result, [...context.path, '$index']); +} + +List _jsonPathSplit(String path) { + return path.split('.').map((String key) { + final int? index = int.tryParse(key); + if (index != null) { + return index; + } else { + return key; + } + }).toList(); +} + +/// Returns the value at `path` of a JSON tree. +/// +/// The path is split using `.`. Integral elements are considered as array +/// indexes, while others are considered as map indexes. +/// +/// If the final result is not of type `T`, throws an `ArgumentError`. +JsonContext jsonGetPath(JsonContext context, String path) { + JsonContext current = context; + void jsonGetKeyOrIndex(dynamic key, int depth) { + assert(key is String || key is int, 'Key at $depth is a ${key.runtimeType}.'); + if (key is String) { + current = jsonGetKey(current as JsonContext, key); + } else if (key is int) { + current = jsonGetIndex(current as JsonContext, key); + } else { + assert(false); + } + } + void jsonGetKeyOrIndexForNext(dynamic key, dynamic nextKey, int depth) { + assert(nextKey is String || nextKey is int, 'Key at ${depth + 1} is a ${key.runtimeType}.'); + if (nextKey is String) { + jsonGetKeyOrIndex(key, depth); + } else if (nextKey is int) { + jsonGetKeyOrIndex(key, depth); + } else { + assert(false); + } + } + + final List pathSegments = _jsonPathSplit(path); + for (int depth = 0; depth < pathSegments.length; depth += 1) { + if (depth != pathSegments.length - 1) { + jsonGetKeyOrIndexForNext(pathSegments[depth], pathSegments[depth + 1], depth); + } else { + jsonGetKeyOrIndex(pathSegments[depth], depth); + } + } + return current as JsonContext; +} diff --git a/tools/gen_web_locale_keymap/lib/layout_types.dart b/tools/gen_web_locale_keymap/lib/layout_types.dart new file mode 100644 index 0000000000000..d7e48ffa5932f --- /dev/null +++ b/tools/gen_web_locale_keymap/lib/layout_types.dart @@ -0,0 +1,63 @@ +// Copyright 2014 The Flutter Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +/// The platform that the browser is running on. +enum LayoutPlatform { + /// Windows. + win, + /// Linux. + linux, + /// MacOS or iOS. + darwin, +} + +// The length of [LayoutEntry.printable]. +const int _kPrintableLength = 4; + +/// Describes the characters that a physical keyboard key will be mapped to +/// under different modifier states, for a given language on a given +/// platform. +class LayoutEntry { + /// Create a layout entry. + LayoutEntry(this.printables) + : assert(printables.length == _kPrintableLength); + + /// The printable characters that a key should be mapped to under different + /// modifier states. + /// + /// The [printables] always have a length of 4, corresponding to "without any + /// modifiers", "with Shift", "with AltGr", and "with Shift and AltGr" + /// respectively. + /// + /// Some values might be empty. It doesn't mean that these combinations will + /// have an empty KeyboardKey.key, but usually these values are trivial, + /// i.e. same as their non-modified counterparts. + /// + /// Some other values can be [kDeadKey]s. Dead keys are non-printable accents + /// that will be combined into the following letter character. + final List printables; + + /// An empty [LayoutEntry] that produces dead keys under all conditions. + static final LayoutEntry empty = LayoutEntry( + const ['', '', '', '']); + + /// The value of KeyboardEvent.key for dead keys. + static const String kDeadKey = 'Dead'; +} + +/// Describes the characters that all goal keys will be mapped to for a given +/// language on a given platform. +class Layout { + /// Create a [Layout]. + const Layout(this.language, this.platform, this.entries); + + /// The language being used. + final String language; + + /// The platform that the browser is running on. + final LayoutPlatform platform; + + /// Maps from DOM `KeyboardKey.code`s to the characters they produce. + final Map entries; +} diff --git a/tools/gen_web_locale_keymap/pubspec.yaml b/tools/gen_web_locale_keymap/pubspec.yaml new file mode 100644 index 0000000000000..363b3b533e33c --- /dev/null +++ b/tools/gen_web_locale_keymap/pubspec.yaml @@ -0,0 +1,20 @@ +name: gen_web_keyboard_layouts +description: Generates keyboard layouts for Web from external sources. + +environment: + sdk: ">=2.18.0-0 <3.0.0" + +dependencies: + args: any + http: ^0.13.4 + path: ^1.8.1 + meta: any + +dev_dependencies: + test: ^1.21.1 + +dependency_overrides: + args: + path: ../../../third_party/dart/third_party/pkg/args + meta: + path: ../../../third_party/dart/pkg/meta diff --git a/tools/licenses/lib/licenses.dart b/tools/licenses/lib/licenses.dart index 2a6e8ee190e97..34440e4426ba6 100644 --- a/tools/licenses/lib/licenses.dart +++ b/tools/licenses/lib/licenses.dart @@ -14,72 +14,66 @@ class FetchedContentsOf extends Key { FetchedContentsOf(dynamic value) : super(v enum LicenseType { unknown, bsd, gpl, lgpl, mpl, afl, mit, freetype, apache, apacheNotice, eclipse, ijg, zlib, icu, apsl, libpng, openssl, vulkan, bison } LicenseType convertLicenseNameToType(String? name) { - switch (name) { - case 'Apache': + switch (name?.toLowerCase()) { + case 'apache': case 'apache-license-2.0': - case 'LICENSE-APACHE-2.0.txt': - case 'Apache-2.0.txt': - case 'LICENSE.vulkan': + case 'license-apache-2.0.txt': + case 'apache-2.0.txt': + case 'license.vulkan': return LicenseType.apache; - case 'BSD': - case 'BSD.txt': - case 'BSD-3-Clause.txt': + case 'bsd': + case 'bsd.txt': + case 'bsd-3-clause.txt': return LicenseType.bsd; - case 'LICENSE-LGPL-2': - case 'LICENSE-LGPL-2.1': - case 'COPYING-LGPL-2.1': + case 'license-lgpl-2': + case 'license-lgpl-2.1': + case 'copying-lgpl-2.1': return LicenseType.lgpl; - case 'COPYING-GPL-3': - case 'GPL-3.0-only.txt': + case 'copying-gpl-3': + case 'gpl-3.0-only.txt': return LicenseType.gpl; - case 'FTL.TXT': + case 'ftl.txt': return LicenseType.freetype; case 'zlib.h': return LicenseType.zlib; case 'png.h': return LicenseType.libpng; - case 'ICU': + case 'icu': return LicenseType.icu; - case 'Apple Public Source License': + case 'apple public source license': return LicenseType.apsl; - case 'OpenSSL': + case 'openssl': return LicenseType.openssl; - case 'LICENSE.MPLv2': - case 'COPYING-MPL-1.1': + case 'license.mplv2': + case 'copying-mpl-1.1': return LicenseType.mpl; - case 'COPYRIGHT.vulkan': + case 'copyright.vulkan': return LicenseType.vulkan; - case 'LICENSE.MIT': - case 'MIT.txt': + case 'license.mit': + case 'mit.txt': return LicenseType.mit; // common file names that don't say what the type is - case 'COPYING': - case 'COPYING.txt': - case 'COPYING.LIB': // lgpl usually - case 'COPYING.RUNTIME': // gcc exception usually - case 'LICENSE': - case 'LICENSE.md': + case 'copying': + case 'copying.txt': + case 'copying.lib': // lgpl usually + case 'copying.runtime': // gcc exception usually + case 'license': + case 'license.md': case 'license.html': - case 'LICENSE.txt': - case 'LICENSE.TXT': - case 'LICENSE.cssmin': - case 'NOTICE': - case 'NOTICE.txt': - case 'Copyright': - case 'copyright': case 'license.txt': + case 'license.cssmin': + case 'notice': + case 'notice.txt': + case 'copyright': return LicenseType.unknown; // particularly weird file names - case 'COPYRIGHT.musl': - case 'LICENSE-APPLE': - case 'extreme.indiana.edu.license.TXT': + case 'copyright.musl': + case 'license-apple': case 'extreme.indiana.edu.license.txt': - case 'javolution.license.TXT': case 'javolution.license.txt': case 'libyaml-license.txt': case 'license.patch': case 'license.rst': - case 'LICENSE.rst': case 'mh-bsd-gcc': case 'pivotal.labs.license.txt': return LicenseType.unknown; diff --git a/tools/licenses/lib/patterns.dart b/tools/licenses/lib/patterns.dart index 1d6bd79fc2920..fe23298b197b7 100644 --- a/tools/licenses/lib/patterns.dart +++ b/tools/licenses/lib/patterns.dart @@ -57,6 +57,7 @@ final List copyrightStatementLeadingPatterns = [ RegExp(r'^ *(?:Portions(?: are)? )?Copyright .+$', caseSensitive: false), RegExp(r'^.*All rights? reserved\.$', caseSensitive: false), RegExp(r'^ *\(C\) .+$', caseSensitive: false), + RegExp(r'^Copyright \(C\) .+$', caseSensitive: false), RegExp(r'^:copyright: .+$', caseSensitive: false), RegExp(r'[-_a-zA-Z0-9()]+ function provided freely by .+'), RegExp(r'^.+ optimized code \(C\) COPYRIGHT .+$', caseSensitive: false), @@ -78,6 +79,7 @@ final List copyrightStatementPatterns = [ RegExp(r'^\(Version [-0-9.:, ]+ Copyright .+\)$', caseSensitive: false), RegExp(r'^.*(?:All )?rights? reserved\.$', caseSensitive: false), RegExp(r'^ *\(C\) .+$', caseSensitive: false), + RegExp(r'^Copyright \(C\) .+$', caseSensitive: false), RegExp(r'^:copyright: .+$', caseSensitive: false), RegExp(r'^ *[0-9][0-9][0-9][0-9].+ [<(].+@.+[)>]$'), RegExp(r'^ [^ ].* [<(].+@.+[)>]$'), // that's exactly the number of spaces to line up with the X if "Copyright (c) 2011 X" is on the previous line @@ -150,7 +152,7 @@ final List copyrightStatementPatterns = [ RegExp(r'^California, Lawrence Berkeley Laboratory\.$'), RegExp(r'^ *Condition of use and distribution are the same than zlib :$'), - RegExp(r'^The MIT License:$'), + RegExp(r'^(The )?MIT License:?$'), RegExp(r'^$'), // TODO(ianh): file an issue on what happens if you omit the close quote @@ -554,6 +556,21 @@ final List csReferencesByFilename = extraImportsMap = { RegExp('skwasm_(stub|impl)'): "import 'dart:_skwasm_stub' if (dart.library.ffi) 'dart:_skwasm_impl';", 'engine': "import 'dart:_engine';", 'web_unicode': "import 'dart:_web_unicode';", + 'web_locale_keymap': "import 'dart:_web_locale_keymap' as locale_keymap;", }; // Rewrites the "package"-style web ui library into a dart:ui implementation. diff --git a/web_sdk/test/sdk_rewriter_test.dart b/web_sdk/test/sdk_rewriter_test.dart index ffe6c40867f95..54902cfc14e1e 100644 --- a/web_sdk/test/sdk_rewriter_test.dart +++ b/web_sdk/test/sdk_rewriter_test.dart @@ -130,17 +130,21 @@ void printSomething() { expect(getExtraImportsForLibrary('engine'), [ "import 'dart:_skwasm_stub' if (dart.library.ffi) 'dart:_skwasm_impl';", "import 'dart:_web_unicode';", + "import 'dart:_web_locale_keymap' as locale_keymap;", ]); expect(getExtraImportsForLibrary('skwasm_stub'), [ "import 'dart:_engine';", "import 'dart:_web_unicode';", + "import 'dart:_web_locale_keymap' as locale_keymap;", ]); expect(getExtraImportsForLibrary('skwasm_impl'), [ "import 'dart:_engine';", "import 'dart:_web_unicode';", + "import 'dart:_web_locale_keymap' as locale_keymap;", ]); // Other libraries (should not have extra imports). expect(getExtraImportsForLibrary('web_unicode'), isEmpty); + expect(getExtraImportsForLibrary('web_locale_keymap'), isEmpty); }); }