From cc0ddffd61e4e1bdec0e86ceb5fd6b8cd264d4d3 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 5 Sep 2022 10:14:10 +1000 Subject: [PATCH] feat(developer): add vkey support vkeyMaps table compiler, unit test, and fixes to json schema. Adds a vkeyMaps entry to the basic e2e test. us-virtual-keys.ts has a new mapping for CLDR VKey Enums, but I will tweak this in an upcoming commit to use constants instead of hard-coded values. --- .../kmldmlc/src/keyman/compiler/compiler.ts | 12 ++-- .../kmldmlc/src/keyman/compiler/messages.ts | 18 ++++- .../src/keyman/compiler/section-compiler.ts | 4 ++ .../src/kmldmlc/src/keyman/compiler/vkey.ts | 68 +++++++++++++++++++ .../ldml-keyboard/ldml-keyboard-xml-reader.ts | 1 + .../keyman/ldml-keyboard/ldml-keyboard-xml.ts | 10 +++ .../keyman/ldml-keyboard/us-virtual-keys.ts | 56 +++++++++++++++ developer/src/kmldmlc/test/fixtures/basic.txt | 21 +++++- developer/src/kmldmlc/test/fixtures/basic.xml | 4 ++ .../sections/vkey/invalid-from-vkey.xml | 18 +++++ .../sections/vkey/invalid-repeated-vkey.xml | 16 +++++ .../sections/vkey/invalid-to-vkey.xml | 15 ++++ .../test/fixtures/sections/vkey/minimal.xml | 17 +++++ .../test/fixtures/sections/vkey/redundant.xml | 15 ++++ .../fixtures/sections/vkey/same-target.xml | 16 +++++ developer/src/kmldmlc/test/test-vkey.ts | 66 ++++++++++++++++++ .../techpreview/ldml-keyboard.schema.json | 5 +- 17 files changed, 349 insertions(+), 13 deletions(-) create mode 100644 developer/src/kmldmlc/src/keyman/compiler/vkey.ts create mode 100644 developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-from-vkey.xml create mode 100644 developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-repeated-vkey.xml create mode 100644 developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-to-vkey.xml create mode 100644 developer/src/kmldmlc/test/fixtures/sections/vkey/minimal.xml create mode 100644 developer/src/kmldmlc/test/fixtures/sections/vkey/redundant.xml create mode 100644 developer/src/kmldmlc/test/fixtures/sections/vkey/same-target.xml create mode 100644 developer/src/kmldmlc/test/test-vkey.ts diff --git a/developer/src/kmldmlc/src/keyman/compiler/compiler.ts b/developer/src/kmldmlc/src/keyman/compiler/compiler.ts index 643bc86442..9f797b3611 100644 --- a/developer/src/kmldmlc/src/keyman/compiler/compiler.ts +++ b/developer/src/kmldmlc/src/keyman/compiler/compiler.ts @@ -1,5 +1,4 @@ -import { constants } from '@keymanapp/ldml-keyboard-constants'; -import KMXPlusFile, { Vkey } from '../kmx/kmx-plus'; +import KMXPlusFile from '../kmx/kmx-plus'; import LDMLKeyboardXMLSourceFile from '../ldml-keyboard/ldml-keyboard-xml'; import LDMLKeyboardXMLSourceFileReader from '../ldml-keyboard/ldml-keyboard-xml-reader'; import CompilerCallbacks from './callbacks'; @@ -7,12 +6,14 @@ import { KeysCompiler } from './keys'; import { LocaCompiler } from './loca'; import { MetaCompiler } from './meta'; import { NameCompiler } from './name'; +import { VkeyCompiler } from './vkey'; const SECTION_COMPILERS = [ KeysCompiler, LocaCompiler, MetaCompiler, - NameCompiler + NameCompiler, + VkeyCompiler ]; export default class Compiler { @@ -67,15 +68,14 @@ export default class Compiler { } const sect = section.compile(); if(!sect) { + // This should not really happen -- validate() should be telling us + // if something is going to fail to compiler passed = false; continue; } kmx.kmxplus[section.id] = sect as any; } - // TEMP until we have a Vkey compiler - kmx.kmxplus[constants.section.vkey] = new Vkey() as any; - return passed ? kmx : null; } } diff --git a/developer/src/kmldmlc/src/keyman/compiler/messages.ts b/developer/src/kmldmlc/src/keyman/compiler/messages.ts index 90bab70036..fdeef58423 100644 --- a/developer/src/kmldmlc/src/keyman/compiler/messages.ts +++ b/developer/src/kmldmlc/src/keyman/compiler/messages.ts @@ -10,7 +10,7 @@ export enum CompilerErrorSeverity { }; const m = (code: number, message: string) => { return { code, message } }; -// const SevInfo = CompilerErrorSeverity.Info; +const SevInfo = CompilerErrorSeverity.Info; const SevHint = CompilerErrorSeverity.Hint; // const SevWarn = CompilerErrorSeverity.Warn; const SevError = CompilerErrorSeverity.Error; @@ -45,6 +45,22 @@ export class CompilerMessages { m(this.HINT_LocaleIsNotMinimalAndClean, `Locale '${sourceLocale}' is not minimal or correctly formatted and should be '${locale}'`); static HINT_LocaleIsNotMinimalAndClean = SevHint | 0x0008; + static Error_VkeyIsNotValid = (vkey: string) => + m(this.ERROR_VkeyIsNotValid, `Virtual key '${vkey}' is not found in the CLDR VKey Enum table.`); + static ERROR_VkeyIsNotValid = SevError | 0x0009; + + static Hint_VkeyMapIsRedundant = (vkey: string) => + m(this.HINT_VkeyMapIsRedundant, `Virtual key '${vkey}' is mapped to itself, which is redundant.`); + static HINT_VkeyMapIsRedundant = SevHint | 0x000A; + + static Error_VkeyMapIsRepeated = (vkey: string) => + m(this.ERROR_VkeyMapIsRepeated, `Virtual key '${vkey}' has more than one VkeyMap entry.`); + static ERROR_VkeyMapIsRepeated = SevError | 0x000B; + + static Info_MultipleVkeyMapsHaveSameTarget = (vkey: string) => + m(this.INFO_MultipleVkeyMapsHaveSameTarget, `Target virtual key '${vkey}' has multiple source mappings, which may be an error.`); + static INFO_MultipleVkeyMapsHaveSameTarget = SevInfo | 0x000C; + static severityName(code: number): string { let severity = code & CompilerErrorSeverity.Severity_Mask; switch(severity) { diff --git a/developer/src/kmldmlc/src/keyman/compiler/section-compiler.ts b/developer/src/kmldmlc/src/keyman/compiler/section-compiler.ts index fb263c4dab..9400be2fbf 100644 --- a/developer/src/kmldmlc/src/keyman/compiler/section-compiler.ts +++ b/developer/src/kmldmlc/src/keyman/compiler/section-compiler.ts @@ -16,6 +16,10 @@ export class SectionCompiler { return null; } + public get required(): boolean { + return true; + } + public compile(): Section { return null; } diff --git a/developer/src/kmldmlc/src/keyman/compiler/vkey.ts b/developer/src/kmldmlc/src/keyman/compiler/vkey.ts new file mode 100644 index 0000000000..8ce8811190 --- /dev/null +++ b/developer/src/kmldmlc/src/keyman/compiler/vkey.ts @@ -0,0 +1,68 @@ +import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { Vkey } from "../kmx/kmx-plus"; +import { LdmlVkeyNames } from "../ldml-keyboard/us-virtual-keys"; +import { CompilerMessages } from "./messages"; +import { SectionCompiler } from "./section-compiler"; + +export class VkeyCompiler extends SectionCompiler { + + public get id() { + return constants.section.vkey; + } + + public get required(): boolean { + return !!this.keyboard.vkeyMaps; + } + + public validate(): boolean { + let valid = true; + if(this.keyboard.vkeyMaps) { + let from: string[] = [], to: string[] = []; + + this.keyboard.vkeyMaps.vkeyMap.forEach(vk => { + if(LdmlVkeyNames[vk.from] === undefined) { + this.callbacks.reportMessage(CompilerMessages.Error_VkeyIsNotValid(vk.from)); + valid = false; + } + + if(LdmlVkeyNames[vk.to] === undefined) { + this.callbacks.reportMessage(CompilerMessages.Error_VkeyIsNotValid(vk.to)); + valid = false; + } + + if(vk.from == vk.to) { + this.callbacks.reportMessage(CompilerMessages.Hint_VkeyMapIsRedundant(vk.from)); + } + + if(from.find(svk => svk == vk.from)) { + this.callbacks.reportMessage(CompilerMessages.Error_VkeyMapIsRepeated(vk.from)); + valid = false; + } + from.push(vk.from); + + if(to.find(svk => svk == vk.to)) { + this.callbacks.reportMessage(CompilerMessages.Info_MultipleVkeyMapsHaveSameTarget(vk.to)); + } + to.push(vk.to); + }); + } + return valid; + } + + public compile(): Vkey { + let result = new Vkey(); + if(!this.keyboard.vkeyMaps) { + return result; + } + + result.vkeys = this.keyboard.vkeyMaps?.vkeyMap.map(vk => { + return { + vkey: LdmlVkeyNames[vk.from], + target: LdmlVkeyNames[vk.to] + }; + }); + // Sort according to vkey binary order, per C7043 + result.vkeys.sort((a,b) => a.vkey - b.vkey); + return result; + } +} diff --git a/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml-reader.ts b/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml-reader.ts index 38ac91f649..386625923d 100644 --- a/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -30,6 +30,7 @@ export default class LDMLKeyboardXMLSourceFileReader { box(source?.keyboard, 'layerMaps'); box(source?.keyboard?.names, 'name'); + box(source?.keyboard?.vkeyMaps, 'vkeyMap'); box(source?.keyboard?.keys, 'key'); box(source?.keyboard?.locales, 'locale'); if(source?.keyboard?.layerMaps) { diff --git a/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml.ts b/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml.ts index ee812447d4..abc3187911 100644 --- a/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml.ts +++ b/developer/src/kmldmlc/src/keyman/ldml-keyboard/ldml-keyboard-xml.ts @@ -25,6 +25,7 @@ export interface LKKeyboard { settings?: LKSettings; keys?: LKKeys; layerMaps?: LKLayerMaps[]; + vkeyMaps?: LKVkeyMaps; }; export interface LKLocales { @@ -82,3 +83,12 @@ export interface LKLayerMap { export interface LKRow { keys?: string; }; + +export interface LKVkeyMaps { + vkeyMap?: LKVkeyMap[]; +}; + +export interface LKVkeyMap { + from?: string; + to?: string; +}; \ No newline at end of file diff --git a/developer/src/kmldmlc/src/keyman/ldml-keyboard/us-virtual-keys.ts b/developer/src/kmldmlc/src/keyman/ldml-keyboard/us-virtual-keys.ts index 6dd58d5914..6486666570 100644 --- a/developer/src/kmldmlc/src/keyman/ldml-keyboard/us-virtual-keys.ts +++ b/developer/src/kmldmlc/src/keyman/ldml-keyboard/us-virtual-keys.ts @@ -137,3 +137,59 @@ export const USVirtualKeyMap: number[][] = [ // [shift] * Z X C V B N M , . / [shift] *=oE2 [ k.K_oE2, k.K_Z, k.K_X, k.K_C, k.K_V, k.K_B, k.K_N, k.K_M, k.K_COMMA, k.K_PERIOD, k.K_SLASH ] ]; + +/** + * Maps LDML VKey Names from CLDR VKey Enum in TR35 to Keyman virtual key codes + */ +export const LdmlVkeyNames: Record = { + 'SPACE': 0x20, // A03 + '0': 0x30, // E10 + '1': 0x31, // E01 + '2': 0x32, // E02 + '3': 0x33, // E03 + '4': 0x34, // E04 + '5': 0x35, // E05 + '6': 0x36, // E06 + '7': 0x37, // E07 + '8': 0x38, // E08 + '9': 0x39, // E09 + 'A': 0x41, // C01 + 'B': 0x42, // B05 + 'C': 0x43, // B03 + 'D': 0x44, // C03 + 'E': 0x45, // D03 + 'F': 0x46, // C04 + 'G': 0x47, // C05 + 'H': 0x48, // C06 + 'I': 0x49, // D08 + 'J': 0x4A, // C07 + 'K': 0x4B, // C08 + 'L': 0x4C, // C09 + 'M': 0x4D, // B07 + 'N': 0x4E, // B06 + 'O': 0x4F, // D09 + 'P': 0x50, // D10 + 'Q': 0x51, // D01 + 'R': 0x52, // D04 + 'S': 0x53, // C02 + 'T': 0x54, // D05 + 'U': 0x55, // D07 + 'V': 0x56, // B05 + 'W': 0x57, // D02 + 'X': 0x58, // B02 + 'Y': 0x59, // D06 + 'Z': 0x5A, // B01 + 'SEMICOLON': 0xBA, // C10 + 'EQUAL': 0xBB, // E12 + 'COMMA': 0xBC, // B08 + 'HYPHEN': 0xBD, // E11 + 'PERIOD': 0xBE, // B09 + 'SLASH': 0xBF, // B10 + 'GRAVE': 0xC0, // E00 + 'LBRACKET': 0xDB, // D11 + 'BACKSLASH': 0xDC, // D13 + 'RBRACKET': 0xDD, // D12 + 'QUOTE': 0xDE, // C11 + 'LESS-THAN': 0xE2, // B00 102nd key on European layouts, right of left shift. + 'ABNT2': 0xC1, // B11 Extra key, left of right-shift +}; \ No newline at end of file diff --git a/developer/src/kmldmlc/test/fixtures/basic.txt b/developer/src/kmldmlc/test/fixtures/basic.txt index b4ab71ecca..9700652d3d 100644 --- a/developer/src/kmldmlc/test/fixtures/basic.txt +++ b/developer/src/kmldmlc/test/fixtures/basic.txt @@ -14,13 +14,18 @@ # xxd -g 1 -l 12 /tmp/basic.kmx | cut -d' ' -f 10-13 # # rm /tmp/basic.kmx # or you may wish to examine it in more detail # +# To generate a binary file for comparison purposes: +# +# node ../../../common/tools/hextobin/build/hextobin.js test/fixtures/basic.txt test/fixtures/basic-h.kmx +# +# block(kmxheader) # struct COMP_KEYBOARD { 4b 58 54 53 # KMX_DWORD dwIdentifier; // 0000 Keyman compiled keyboard id 00 10 00 00 # KMX_DWORD dwFileVersion; // 0004 Version of the file - Keyman 4.0 is 0x0400 - 62 78 ee 07 # KMX_DWORD dwCheckSum; // 0008 As stored in keyboard + 4c f9 ee bd # KMX_DWORD dwCheckSum; // 0008 As stored in keyboard 00 00 00 00 # KMX_DWORD KeyboardID; // 000C as stored in HKEY_LOCAL_MACHINE//system//currentcontrolset//control//keyboard layouts 01 00 00 00 # KMX_DWORD IsRegistered; // 0010 00 00 00 00 # KMX_DWORD version; // 0014 keyboard version @@ -52,7 +57,7 @@ block(sect) # struct COMP_KMXPLUS_SECT { 73 65 63 74 # KMX_DWORD header.ident; // 0000 Section name sizeof(sect) # KMX_DWORD header.size; // 0004 Section length diff(sect,eof) # KMX_DWORD total; // 0008 KMXPlus entire length - 05 00 00 00 # KMX_DWORD count; // 000C number of section headers + 06 00 00 00 # KMX_DWORD count; // 000C number of section headers # }; # Next sections are sect entries # KMX_DWORD sect; // 0010+ Section identity @@ -73,6 +78,9 @@ block(sect) # struct COMP_KMXPLUS_SECT { 73 74 72 73 diff(sect,strs) + 76 6b 65 79 + diff(sect,vkey) + block(keys) # struct COMP_KMXPLUS_KEYS { 6b 65 79 73 # KMX_DWORD header.ident; // 0000 Section name - keys sizeof(keys) # KMX_DWORD header.size; // 0004 Section length @@ -155,4 +163,13 @@ block(strs) # struct COMP_KMXPLUS_STRS { block(endstrs) # end of strs block +block(vkey) # struct COMP_KMXPLUS_VKEY { + 76 6b 65 79 # KMX_DWORD header.ident; // 0000 Section name - vkey + sizeof(vkey) # KMX_DWORD header.size; // 0004 Section length + 01 00 00 00 # KMX_DWORD count; // 0008 Number of vkey maps + 00 00 00 00 # KMX_DWORD reserved; // 000C padding + + 51 00 00 00 41 00 00 00 # KMX_DWORD vkey; KMX_DWORD target; // K_Q => K_A + # }; + block(eof) # end of file \ No newline at end of file diff --git a/developer/src/kmldmlc/test/fixtures/basic.xml b/developer/src/kmldmlc/test/fixtures/basic.xml index 4dacef6964..57755aeae1 100644 --- a/developer/src/kmldmlc/test/fixtures/basic.xml +++ b/developer/src/kmldmlc/test/fixtures/basic.xml @@ -12,6 +12,10 @@ + + + + diff --git a/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-from-vkey.xml b/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-from-vkey.xml new file mode 100644 index 0000000000..b186b9e2c0 --- /dev/null +++ b/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-from-vkey.xml @@ -0,0 +1,18 @@ + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-repeated-vkey.xml b/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-repeated-vkey.xml new file mode 100644 index 0000000000..092d2d68d3 --- /dev/null +++ b/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-repeated-vkey.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-to-vkey.xml b/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-to-vkey.xml new file mode 100644 index 0000000000..2b8b9beb0e --- /dev/null +++ b/developer/src/kmldmlc/test/fixtures/sections/vkey/invalid-to-vkey.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/developer/src/kmldmlc/test/fixtures/sections/vkey/minimal.xml b/developer/src/kmldmlc/test/fixtures/sections/vkey/minimal.xml new file mode 100644 index 0000000000..e5b51ddae1 --- /dev/null +++ b/developer/src/kmldmlc/test/fixtures/sections/vkey/minimal.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/developer/src/kmldmlc/test/fixtures/sections/vkey/redundant.xml b/developer/src/kmldmlc/test/fixtures/sections/vkey/redundant.xml new file mode 100644 index 0000000000..811aa3fe87 --- /dev/null +++ b/developer/src/kmldmlc/test/fixtures/sections/vkey/redundant.xml @@ -0,0 +1,15 @@ + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/developer/src/kmldmlc/test/fixtures/sections/vkey/same-target.xml b/developer/src/kmldmlc/test/fixtures/sections/vkey/same-target.xml new file mode 100644 index 0000000000..0c721e31b4 --- /dev/null +++ b/developer/src/kmldmlc/test/fixtures/sections/vkey/same-target.xml @@ -0,0 +1,16 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/developer/src/kmldmlc/test/test-vkey.ts b/developer/src/kmldmlc/test/test-vkey.ts new file mode 100644 index 0000000000..848821a079 --- /dev/null +++ b/developer/src/kmldmlc/test/test-vkey.ts @@ -0,0 +1,66 @@ +import 'mocha'; +import { assert } from 'chai'; +import { VkeyCompiler } from '../src/keyman/compiler/vkey'; +import { CompilerCallbacks, loadSectionFixture } from './helpers'; +import { Vkey } from '../src/keyman/kmx/kmx-plus'; +import { CompilerMessages } from '../src/keyman/compiler/messages'; +import { USVirtualKeyCodes } from '../src/keyman/ldml-keyboard/us-virtual-keys'; + +describe('name', function () { + this.slow(500); // 0.5 sec -- json schema validation takes a while + + it('should compile minimal vkey data', function() { + const callbacks = new CompilerCallbacks(); + let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/minimal.xml', callbacks) as Vkey; + assert.equal(callbacks.messages.length, 0); + + assert.equal(vkey.vkeys.length, 4); + // Note, final order is sorted by `vkey` member + assert.deepEqual(vkey.vkeys[0], {vkey: USVirtualKeyCodes.K_A, target: USVirtualKeyCodes.K_Q}); + assert.deepEqual(vkey.vkeys[1], {vkey: USVirtualKeyCodes.K_Q, target: USVirtualKeyCodes.K_A}); + assert.deepEqual(vkey.vkeys[2], {vkey: USVirtualKeyCodes.K_W, target: USVirtualKeyCodes.K_Z}); + assert.deepEqual(vkey.vkeys[3], {vkey: USVirtualKeyCodes.K_Z, target: USVirtualKeyCodes.K_W}); + }); + + it('should hint on redundant data', function() { + const callbacks = new CompilerCallbacks(); + let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/redundant.xml', callbacks) as Vkey; + assert.isNotNull(vkey); + assert.equal(callbacks.messages.length, 1); + assert.deepEqual(callbacks.messages[0], CompilerMessages.Hint_VkeyMapIsRedundant('A')); + }); + + it('should report an info message if same target found', function() { + const callbacks = new CompilerCallbacks(); + let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/same-target.xml', callbacks) as Vkey; + assert.isNotNull(vkey); + assert.equal(callbacks.messages.length, 1); + assert.deepEqual(callbacks.messages[0], CompilerMessages.Info_MultipleVkeyMapsHaveSameTarget('Q')); + }); + + it('should error on invalid "from" vkey', function() { + const callbacks = new CompilerCallbacks(); + let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-from-vkey.xml', callbacks) as Vkey; + assert.isNull(vkey); + assert.equal(callbacks.messages.length, 2); + assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid('q')); + assert.deepEqual(callbacks.messages[1], CompilerMessages.Error_VkeyIsNotValid('HYFEN')); + }); + + it('should error on invalid "to" vkey', function() { + const callbacks = new CompilerCallbacks(); + let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-to-vkey.xml', callbacks) as Vkey; + assert.isNull(vkey); + assert.equal(callbacks.messages.length, 1); + assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyIsNotValid('A-ACUTE')); + }); + + it('should error on repeated vkeys', function() { + const callbacks = new CompilerCallbacks(); + let vkey = loadSectionFixture(VkeyCompiler, 'sections/vkey/invalid-repeated-vkey.xml', callbacks) as Vkey; + assert.isNull(vkey); + assert.equal(callbacks.messages.length, 1); + assert.deepEqual(callbacks.messages[0], CompilerMessages.Error_VkeyMapIsRepeated('A')); + }); +}); + diff --git a/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboard.schema.json b/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboard.schema.json index fb02a14e8e..de49f43cb7 100644 --- a/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboard.schema.json +++ b/resources/standards-data/ldml-keyboards/techpreview/ldml-keyboard.schema.json @@ -29,10 +29,7 @@ "$ref": "#/definitions/settings" }, "vkeyMaps": { - "type": "array", - "items": { - "$ref": "#/definitions/vkeyMaps" - } + "$ref": "#/definitions/vkeyMaps" }, "displayMap": { "type": "array",