diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-key2.ts b/common/web/types/src/kmx/kmx-plus-builder/build-key2.ts new file mode 100644 index 0000000000..edff5520b0 --- /dev/null +++ b/common/web/types/src/kmx/kmx-plus-builder/build-key2.ts @@ -0,0 +1,145 @@ + +import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { Key2Flick, KMXPlusData, StrsItem } from "../kmx-plus.js"; +import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { build_list_index, BUILDER_LIST } from "./build-list.js"; +import { BUILDER_SECTION } from "./builder-section.js"; + +/* ------------------------------------------------------------------ + * key2 section + ------------------------------------------------------------------ */ + +/** + * This struct is a single in the key2 keybag + */ +interface BUILDER_KEY2_KEY { + vkey: number; // Scan code for the key + to: number; // str or single codepoint + flags: number; + id: number; // str with original key id + _id: string; // original key id, for sorting + switch: number; // str with layer of new l + width: number; // ceil((width||1)*10), so 12 for width 1.2 + longPress: number; // list of longPress sequences + longPressDefault: number; // str with the default longPress target + multiTap: number; // list of multiTap sequences + flicks: number; // index into the flicks[] subtable for this flick list +}; + +/** + * This is a , a list of elements. + */ +interface BUILDER_KEY2_FLICKS { + count: number; // number of BUILDER_KEY2_FLICK entries in this flick list + flick: number; // index into the flick[] subtable of the first flick in the list + id: number; // str with the original id of this flicks + _id: string; // copy of the flicks id, used for sorting during build + _flicks: Key2Flick[]; // temporary copy of Key2Flick object +}; + +/** + * This is a single element. + */ +interface BUILDER_KEY2_FLICK { + directions: number; // list of cardinal/intercardinal directions + flags: number; // + to: number; // str or single codepoint +}; + +/** + * Builder for the 'keys' section + */ +export interface BUILDER_KEY2 extends BUILDER_SECTION { + ident: number; + size: number; + keyCount: number; + flicksCount: number; + flickCount: number; + reserved0: number; + reserved1: number; + reserved2: number; + keys: BUILDER_KEY2_KEY[]; + flicks: BUILDER_KEY2_FLICKS[]; + flick: BUILDER_KEY2_FLICK[]; +}; + +export function build_key2(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_list: BUILDER_LIST): BUILDER_KEY2 { + if(kmxplus.key2.keys.length == 0 && + (kmxplus.key2.flicks.length <= 1)) { // if no keys and only the 'null' flick. + return null; + } + + let key2: BUILDER_KEY2 = { + ident: constants.hex_section_id(constants.section.key2), + size: 0, + keyCount: kmxplus.key2.keys.length, + flicksCount: kmxplus.key2.flicks.length, + flickCount: 0, + reserved0: 0, + reserved1: 0, + reserved2: 0, + keys: [], + flicks: [], + flick: [], + _offset: 0, + }; + + // flicks first: the keys will need to index into the flicks table. + + // Note that per the Key2 class and spec, there is always a flicks=0 meaning 'no flicks' + key2.flicks = kmxplus.key2.flicks.map((flicks) => { + let result : BUILDER_KEY2_FLICKS = { + count: flicks.flicks.length, + flick: key2.flick.length, // index of first flick + id: build_strs_index(sect_strs, flicks.id), + _id: flicks.id.value, + _flicks: flicks.flicks, + }; + return result; + }); + // Sort the flicks array by id + key2.flicks.sort((a, b) => StrsItem.binaryStringCompare(a._id, b._id)); + // now, allocate 'flick' entries for each 'flicks' + key2.flicks.forEach((flicks) => { + flicks._flicks.forEach((flick) => { + key2.flick.push({ + directions: build_list_index(sect_list, flick.directions), + flags: flick.flags, + to: build_strs_index(sect_strs, flick.to), + }); + key2.flickCount++; + }); + }); + + // now, keys + key2.keys = kmxplus.key2.keys.map((key) => { + let result : BUILDER_KEY2_KEY = { + vkey: key.vkey, + to: build_strs_index(sect_strs, key.to), + flags: key.flags, + id: build_strs_index(sect_strs, key.id), + _id: key.id.value, + switch: build_strs_index(sect_strs, key.switch), + width: key.width, + longPress: build_list_index(sect_list, key.longPress), + longPressDefault: build_strs_index(sect_strs, key.longPressDefault), + multiTap: build_list_index(sect_list, key.multiTap), + flicks: key2.flicks.findIndex(v => v._id === (key.flicks || '')), // flicks id='' is the 'null' flicks + }; + // Make sure the flicks were found + if (result.flicks === -1) { + throw new Error(`Key2: Could not find flicks id=${key.flicks} for key=${key.id.value}`); + } + return result; + }); + // sort the keys by id + key2.keys.sort((a, b) => StrsItem.binaryStringCompare(a._id, b._id)); + + let offset = constants.length_key2 + + (constants.length_key2_key * key2.keyCount) + + (constants.length_key2_flick_element * key2.flickCount) + + (constants.length_key2_flick_list * key2.flicksCount); + key2.size = offset; + + return key2; +} diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts b/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts new file mode 100644 index 0000000000..fe35c5d2ef --- /dev/null +++ b/common/web/types/src/kmx/kmx-plus-builder/build-layr.ts @@ -0,0 +1,162 @@ + +import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { KMXPlusData, LayrEntry, LayrRow, StrsItem } from "../kmx-plus.js"; +import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { BUILDER_LIST } from "./build-list.js"; +import { BUILDER_SECTION } from "./builder-section.js"; + +/* ------------------------------------------------------------------ + * layr section - + ------------------------------------------------------------------ */ + +/** + * List of layers, the element + */ +interface BUILDER_LAYR_LIST { + flags: number; + hardware: number; // str - hardware name, see #7986 + layer: number; // index of first layer in the list, in the + count: number; // number of layer entries in the list + minDeviceWidth: number; // width in millimeters + _layers: LayrEntry[]; // original layer entry, for in-memory only +}; + +/** + * element + */ +interface BUILDER_LAYR_LAYER { + id: number; // str of layer id + _id: string; // original layer id, for sorting + modifier: number; // str of modifier string + row: number; // row index into row subtable + _rows: LayrRow[]; // original rows, for in-memory only + count: number; // number of row entries +}; + +/** + * element + */ +interface BUILDER_LAYR_ROW { + key: number; // index into key subtable + count: number; // number of keys +}; + +/** + * portion of keys attribute of + */ +interface BUILDER_LAYR_KEY { + key: number; +}; + +/** + * Builder for the 'keys' section + */ +export interface BUILDER_LAYR extends BUILDER_SECTION { + listCount: number, // number of entries in lists subtable + layerCount: number, // number of entries in layers subtable + rowCount: number, // number of entries in rows subtable + keyCount: number, // number of entries in keys subtable + reserved0: number, // padding + reserved1: number, // padding + lists: BUILDER_LAYR_LIST[], // subtable of elements + layers: BUILDER_LAYR_LAYER[], // subtable of elements + rows: BUILDER_LAYR_ROW[], // subtable of elements + keys: BUILDER_LAYR_KEY[], // subtable of key entries +}; + +export function build_layr(kmxplus: KMXPlusData, sect_strs: BUILDER_STRS, sect_list: BUILDER_LIST): BUILDER_LAYR { + if (!kmxplus.layr?.lists) { + return null; // if there aren't any layers at all (which should be an invalid keyboard) + } + + let layr: BUILDER_LAYR = { + ident: constants.hex_section_id(constants.section.layr), + size: constants.length_layr, + _offset: 0, + listCount: kmxplus.layr.lists.length, + layerCount: 0, // calculated below + rowCount: 0, // calculated below + keyCount: 0, // calculated below + reserved0: 0, + reserved1: 0, + lists: [], + layers: [], + rows: [], + keys: [] + }; + + layr.lists = kmxplus.layr.lists.map((list) => { + const blist: BUILDER_LAYR_LIST = { + flags: list.flags, + hardware: build_strs_index(sect_strs, list.hardware), + layer: null, // to be set below + _layers: list.layers, + count: list.layers.length, + minDeviceWidth: list.minDeviceWidth, + }; + return blist; + }); + // now sort the lists + layr.lists.sort((a, b) => { + const aform = a.flags & constants.layr_list_flags_mask_form; + const bform = b.flags & constants.layr_list_flags_mask_form; + if (aform < bform) { + return -1; + } else if (aform > bform) { + return 1; + } + if (a.minDeviceWidth < b.minDeviceWidth) { + return -1; + } else if (a.minDeviceWidth > b.minDeviceWidth) { + return 1; + } else { + return 0; // same + } + }); + // Now allocate the layers, rows, and keys + layr.lists.forEach((list) => { + list.layer = layr.layers.length; // index to first layer in list + const blayers = list._layers.map((layer) => { + const blayer: BUILDER_LAYR_LAYER = { + _id: layer.id.value, // original id + id: build_strs_index(sect_strs, layer.id), + modifier: build_strs_index(sect_strs, layer.modifier), + row: null, // row ID, to be filled in + _rows: layer.rows, // temporary + count: layer.rows.length, // number of rows + }; + return blayer; + }); + // sort the new layers + blayers.sort((a, b) => StrsItem.binaryStringCompare(a._id, b._id)); + blayers.forEach((layer) => { + layer.row = layr.rows.length; // index to first row in list + layer._rows.forEach((row) => { + const brow: BUILDER_LAYR_ROW = { + key: layr.keys.length, + count: row.keys.length, + }; + row.keys.forEach((key) => { + const bkey: BUILDER_LAYR_KEY = { + key: build_strs_index(sect_strs, key), + }; + layr.keys.push(bkey); + }); + layr.rows.push(brow); + }); + layr.layers.push(layer); + }); + }); + + layr.layerCount = layr.layers.length; + layr.rowCount = layr.rows.length; + layr.keyCount = layr.keys.length; + + let offset = constants.length_layr + + (constants.length_layr_list * layr.listCount) + + (constants.length_layr_entry * layr.layerCount) + + (constants.length_layr_row * layr.rowCount) + + (constants.length_layr_key * layr.keyCount); + layr.size = offset; + return layr; +} diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-list.ts b/common/web/types/src/kmx/kmx-plus-builder/build-list.ts new file mode 100644 index 0000000000..a1ed950463 --- /dev/null +++ b/common/web/types/src/kmx/kmx-plus-builder/build-list.ts @@ -0,0 +1,95 @@ +import { constants } from "@keymanapp/ldml-keyboard-constants"; +import { List, ListItem } from "../kmx-plus.js"; +import { build_strs_index, BUILDER_STRS } from "./build-strs.js"; +import { BUILDER_SECTION } from "./builder-section.js"; + +/* ------------------------------------------------------------------ + * list section + ------------------------------------------------------------------ */ + +/** + * A list entry. + */ +interface BUILDER_LIST_LIST { + index: number; // index into indices[] subtable + count: number; // number of strings in this list + _value: ListItem; // for locating the list during finalization +}; + +interface BUILDER_LIST_INDEX { + str: number; // str for this string + _value: string; // for locating this string during finalization +}; + +/** + * Builder for the 'list' section + */ +export interface BUILDER_LIST extends BUILDER_SECTION { + listCount: number; // Number of lists total in the subtable + indexCount: number; // Total number of indices in the subtable + lists: BUILDER_LIST_LIST[]; + indices: BUILDER_LIST_INDEX[]; +}; + +export function build_list(source_list: List, sect_strs: BUILDER_STRS): BUILDER_LIST { + if(!source_list?.lists?.length) { + // there's always the null list + return null; + } + + let result: BUILDER_LIST = { + ident: constants.hex_section_id(constants.section.list), + size: 0, + _offset: 0, + listCount: source_list.lists.length, + indexCount: 0, + lists: [], + indices: [], + }; + + result.lists = source_list.lists.map(array => { + let list : BUILDER_LIST_LIST = { + index: result.indices.length, // the next indexcount + count: array.length, + _value: array + }; + array.forEach((i) => { + let index : BUILDER_LIST_INDEX = { + // Get the final string index + str: build_strs_index(sect_strs, i.value), + _value: i.value.value, // unwrap the actual string value + }; + result.indices.push(index); // increment the indexCount + result.indexCount++; + }); + return list; + }); + + // Sort the lists. + result.lists.sort((a,b) => a._value.compareTo(b._value)); + + let offset = constants.length_list + + (constants.length_list_item * result.listCount) + + (constants.length_list_index * result.indexCount); + result.size = offset; + + return result; +} + +/** + * Returns the index into the list, analagous to build_strs_index + * @param sect_strs + * @param value + * @returns + */ +export function build_list_index(sect_list: BUILDER_LIST, value: ListItem) { + if(!(value instanceof ListItem)) { + throw new Error('unexpected value '+ value); + } + + let result = sect_list.lists.findIndex(v => v._value === value); + if(result < 0) { + throw new Error('unexpectedly missing ListItem ' + value); // TODO-LDML: it's an array of strs + } + return result; +} diff --git a/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts b/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts index 7cb931fbaa..2c9a43eef8 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/build-strs.ts @@ -23,16 +23,6 @@ export interface BUILDER_STRS extends BUILDER_SECTION { items: BUILDER_STRS_ITEM[]; }; -function binaryStringCompare(a: string, b: string): number { - // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islessthan - if(typeof a != 'string' || typeof b != 'string') { - throw new Error('binaryStringCompare: inputs must be strings'); - } - if(a < b) return -1; - if(a > b) return 1; - return 0; -} - export function build_strs(source_strs: Strs): BUILDER_STRS { let result: BUILDER_STRS = { ident: constants.hex_section_id(constants.section.strs), @@ -44,7 +34,7 @@ export function build_strs(source_strs: Strs): BUILDER_STRS { }; result.items = source_strs.strings.map(item => { return {_value: item.value, length: item.value.length, offset: 0}; }); - result.items.sort((a,b) => binaryStringCompare(a._value, b._value)); + result.items.sort((a,b) => StrsItem.binaryStringCompare(a._value, b._value)); let offset = constants.length_strs + constants.length_strs_item * result.count; // TODO: consider padding diff --git a/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts b/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts index 9c8e30aee7..275db8a904 100644 --- a/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts +++ b/common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts @@ -1,39 +1,52 @@ import * as r from 'restructure'; import { KMXPlusFile } from "../kmx-plus.js"; -import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { constants, SectionIdent } from '@keymanapp/ldml-keyboard-constants'; import { BUILDER_SECTION } from './builder-section.js'; import { BUILDER_SECT, build_sect } from './build-sect.js'; +import { BUILDER_DISP, build_disp } from './build-disp.js'; +import { BUILDER_ELEM, build_elem } from './build-elem.js'; +import { BUILDER_KEY2, build_key2 } from './build-key2.js'; import { BUILDER_KEYS, build_keys } from './build-keys.js'; +import { BUILDER_LAYR, build_layr } from './build-layr.js'; +import { BUILDER_LIST, build_list } from './build-list.js'; import { BUILDER_LOCA, build_loca } from './build-loca.js'; import { BUILDER_META, build_meta } from './build-meta.js'; import { BUILDER_NAME, build_name } from './build-name.js'; -import { BUILDER_STRS, build_strs } from './build-strs.js'; -import { BUILDER_VKEY, build_vkey } from './build-vkey.js'; -import { BUILDER_TRAN, build_tran } from './build-tran.js'; -import { BUILDER_ELEM, build_elem } from './build-elem.js'; import { BUILDER_ORDR, build_ordr } from './build-ordr.js'; -import { BUILDER_DISP, build_disp } from './build-disp.js'; +import { BUILDER_STRS, build_strs } from './build-strs.js'; +import { BUILDER_TRAN, build_tran } from './build-tran.js'; +import { BUILDER_VKEY, build_vkey } from './build-vkey.js'; type BUILDER_BKSP = BUILDER_TRAN; type BUILDER_FINL = BUILDER_TRAN; +type SectionBuilders = { + // [id in SectionIdent]: BUILDER_SECTION; + sect?: BUILDER_SECT; + bksp?: BUILDER_BKSP; + disp?: BUILDER_DISP; + elem?: BUILDER_ELEM; + finl?: BUILDER_FINL; + key2?: BUILDER_KEY2; + keys?: BUILDER_KEYS; + layr?: BUILDER_LAYR; + list?: BUILDER_LIST; + loca?: BUILDER_LOCA; + meta?: BUILDER_META; + name?: BUILDER_NAME; + ordr?: BUILDER_ORDR; + strs?: BUILDER_STRS; + tran?: BUILDER_TRAN; + vkey?: BUILDER_VKEY; +}; + export default class KMXPlusBuilder { private file: KMXPlusFile; //private writeDebug: boolean; - private sect_sect: BUILDER_SECT; - private sect_bksp: BUILDER_BKSP; - private sect_disp: BUILDER_DISP; - private sect_elem: BUILDER_ELEM; - private sect_finl: BUILDER_FINL; - private sect_keys: BUILDER_KEYS; - private sect_loca: BUILDER_LOCA; - private sect_meta: BUILDER_META; - private sect_name: BUILDER_NAME; - private sect_ordr: BUILDER_ORDR; - private sect_strs: BUILDER_STRS; - private sect_tran: BUILDER_TRAN; - private sect_vkey: BUILDER_VKEY; + sect : SectionBuilders = { + + }; constructor(file: KMXPlusFile, _writeDebug: boolean) { this.file = file; @@ -44,21 +57,25 @@ export default class KMXPlusBuilder { const fileSize = this.build(); let file: Uint8Array = new Uint8Array(fileSize); - this.emitSection(file, this.file.COMP_PLUS_SECT, this.sect_sect); - this.emitSection(file, this.file.COMP_PLUS_BKSP, this.sect_bksp); - this.emitSection(file, this.file.COMP_PLUS_DISP, this.sect_disp); - this.emitSection(file, this.file.COMP_PLUS_ELEM, this.sect_elem); + this.emitSection(file, this.file.COMP_PLUS_SECT, this.sect.sect); + // Keep the rest of these in order. + this.emitSection(file, this.file.COMP_PLUS_BKSP, this.sect.bksp); + this.emitSection(file, this.file.COMP_PLUS_DISP, this.sect.disp); + this.emitSection(file, this.file.COMP_PLUS_ELEM, this.sect.elem); this.emitElements(file); - this.emitSection(file, this.file.COMP_PLUS_FINL, this.sect_finl); - this.emitSection(file, this.file.COMP_PLUS_KEYS, this.sect_keys); - this.emitSection(file, this.file.COMP_PLUS_LOCA, this.sect_loca); - this.emitSection(file, this.file.COMP_PLUS_META, this.sect_meta); - this.emitSection(file, this.file.COMP_PLUS_NAME, this.sect_name); - this.emitSection(file, this.file.COMP_PLUS_ORDR, this.sect_ordr); - this.emitSection(file, this.file.COMP_PLUS_STRS, this.sect_strs); + this.emitSection(file, this.file.COMP_PLUS_FINL, this.sect.finl); + this.emitSection(file, this.file.COMP_PLUS_KEY2, this.sect.key2); + this.emitSection(file, this.file.COMP_PLUS_KEYS, this.sect.keys); + this.emitSection(file, this.file.COMP_PLUS_LAYR, this.sect.layr); + this.emitSection(file, this.file.COMP_PLUS_LIST, this.sect.list); + this.emitSection(file, this.file.COMP_PLUS_LOCA, this.sect.loca); + this.emitSection(file, this.file.COMP_PLUS_META, this.sect.meta); + this.emitSection(file, this.file.COMP_PLUS_NAME, this.sect.name); + this.emitSection(file, this.file.COMP_PLUS_ORDR, this.sect.ordr); + this.emitSection(file, this.file.COMP_PLUS_STRS, this.sect.strs); this.emitStrings(file); - this.emitSection(file, this.file.COMP_PLUS_TRAN, this.sect_tran); - this.emitSection(file, this.file.COMP_PLUS_VKEY, this.sect_vkey); + this.emitSection(file, this.file.COMP_PLUS_TRAN, this.sect.tran); + this.emitSection(file, this.file.COMP_PLUS_VKEY, this.sect.vkey); return file; } @@ -66,85 +83,68 @@ export default class KMXPlusBuilder { private build() { // Required sections: sect, strs, loca, meta - // We must prepare the strs and elem sections early so that other sections can + // We must prepare the strs, list, and elem sections early so that other sections can // reference them. However, they will be emitted in alpha order. - this.sect_strs = build_strs(this.file.kmxplus.strs); - this.sect_elem = build_elem(this.file.kmxplus.elem, this.sect_strs); + this.sect.strs = build_strs(this.file.kmxplus.strs); + this.sect.list = build_list(this.file.kmxplus.list, this.sect.strs); + this.sect.elem = build_elem(this.file.kmxplus.elem, this.sect.strs); const build_bksp = build_tran; const build_finl = build_tran; - this.sect_bksp = build_bksp(this.file.kmxplus.bksp, this.sect_strs, this.sect_elem); - this.sect_disp = build_disp(this.file.kmxplus, this.sect_strs); - this.sect_finl = build_finl(this.file.kmxplus.finl, this.sect_strs, this.sect_elem); - this.sect_keys = build_keys(this.file.kmxplus, this.sect_strs); - this.sect_loca = build_loca(this.file.kmxplus, this.sect_strs); - this.sect_meta = build_meta(this.file.kmxplus, this.sect_strs); - this.sect_name = build_name(this.file.kmxplus, this.sect_strs); - this.sect_ordr = build_ordr(this.file.kmxplus, this.sect_strs, this.sect_elem); - this.sect_tran = build_tran(this.file.kmxplus.tran, this.sect_strs, this.sect_elem); - this.sect_vkey = build_vkey(this.file.kmxplus); + this.sect.bksp = build_bksp(this.file.kmxplus.bksp, this.sect.strs, this.sect.elem); + this.sect.disp = build_disp(this.file.kmxplus, this.sect.strs); + this.sect.finl = build_finl(this.file.kmxplus.finl, this.sect.strs, this.sect.elem); + this.sect.key2 = build_key2(this.file.kmxplus, this.sect.strs, this.sect.list); + this.sect.keys = build_keys(this.file.kmxplus, this.sect.strs); + this.sect.layr = build_layr(this.file.kmxplus, this.sect.strs, this.sect.list); + this.sect.loca = build_loca(this.file.kmxplus, this.sect.strs); + this.sect.meta = build_meta(this.file.kmxplus, this.sect.strs); + this.sect.name = build_name(this.file.kmxplus, this.sect.strs); + this.sect.ordr = build_ordr(this.file.kmxplus, this.sect.strs, this.sect.elem); + this.sect.tran = build_tran(this.file.kmxplus.tran, this.sect.strs, this.sect.elem); + this.sect.vkey = build_vkey(this.file.kmxplus); // Finalize the sect (index) section - this.sect_sect = build_sect(); + this.sect.sect = build_sect(); this.finalize_sect(); // must be done last - return this.sect_sect.total; + return this.sect.sect.total; } private finalize_sect() { // 'sect' section - // We always have 'loca', 'meta' and 'strs' - this.sect_sect.count = 3; + this.sect.sect.count = 0; - // Handle optional sections - // TODO: use a loop... - if(this.sect_bksp) { - this.sect_sect.count++; - } - if(this.sect_disp) { - this.sect_sect.count++; - } - if(this.sect_elem) { - this.sect_sect.count++; - } - if(this.sect_finl) { - this.sect_sect.count++; - } - if(this.sect_keys) { - this.sect_sect.count++; - } - if(this.sect_name) { - this.sect_sect.count++; - } - if(this.sect_ordr) { - this.sect_sect.count++; - } - if(this.sect_tran) { - this.sect_sect.count++; - } - if(this.sect_vkey) { - this.sect_sect.count++; - } + Object.keys(constants.section).forEach((sectstr : string) => { + const sect : SectionIdent = constants.section[sectstr]; + if(this.sect[sect] && sect !== 'sect') { + this.sect.sect.count++; + } + }); - this.sect_sect.size = constants.length_sect + constants.length_sect_item * this.sect_sect.count; + this.sect.sect.size = constants.length_sect + constants.length_sect_item * this.sect.sect.count; - let offset = this.sect_sect.size; - offset = this.finalize_sect_item(this.sect_bksp, offset); - offset = this.finalize_sect_item(this.sect_disp, offset); - offset = this.finalize_sect_item(this.sect_elem, offset); - offset = this.finalize_sect_item(this.sect_finl, offset); - offset = this.finalize_sect_item(this.sect_keys, offset); - offset = this.finalize_sect_item(this.sect_loca, offset); - offset = this.finalize_sect_item(this.sect_meta, offset); - offset = this.finalize_sect_item(this.sect_name, offset); - offset = this.finalize_sect_item(this.sect_ordr, offset); - offset = this.finalize_sect_item(this.sect_strs, offset); - offset = this.finalize_sect_item(this.sect_tran, offset); - offset = this.finalize_sect_item(this.sect_vkey, offset); + let offset = this.sect.sect.size; + // Note: in order! Everyone's here except 'sect' which is at offset 0 + offset = this.finalize_sect_item(this.sect.bksp, offset); + offset = this.finalize_sect_item(this.sect.disp, offset); + offset = this.finalize_sect_item(this.sect.elem, offset); + offset = this.finalize_sect_item(this.sect.finl, offset); + offset = this.finalize_sect_item(this.sect.key2, offset); + offset = this.finalize_sect_item(this.sect.keys, offset); + offset = this.finalize_sect_item(this.sect.layr, offset); + offset = this.finalize_sect_item(this.sect.list, offset); + offset = this.finalize_sect_item(this.sect.loca, offset); + offset = this.finalize_sect_item(this.sect.meta, offset); + offset = this.finalize_sect_item(this.sect.name, offset); + offset = this.finalize_sect_item(this.sect.ordr, offset); + offset = this.finalize_sect_item(this.sect.strs, offset); + offset = this.finalize_sect_item(this.sect.tran, offset); + offset = this.finalize_sect_item(this.sect.vkey, offset); - this.sect_sect.total = offset; + this.sect.sect.total = offset; } private finalize_sect_item(sect: BUILDER_SECTION, offset: number): number { @@ -153,7 +153,7 @@ export default class KMXPlusBuilder { return offset; } sect._offset = offset; - this.sect_sect.items.push({sect: sect.ident, offset: offset}); + this.sect.sect.items.push({sect: sect.ident, offset: offset}); // TODO: padding return offset + sect.size; } @@ -165,24 +165,24 @@ export default class KMXPlusBuilder { } private emitStrings(file: Uint8Array) { - for(let item of this.sect_strs.items) { + for(let item of this.sect.strs.items) { if(item._value === '') { // We have a special case for the zero-length string let sbuf = r.uint16le; - file.set(sbuf.toBuffer(0), item.offset + this.sect_strs._offset); + file.set(sbuf.toBuffer(0), item.offset + this.sect.strs._offset); } else { let sbuf = new r.String(null, 'utf16le'); - file.set(sbuf.toBuffer(item._value), item.offset + this.sect_strs._offset); + file.set(sbuf.toBuffer(item._value), item.offset + this.sect.strs._offset); } } } private emitElements(file: Uint8Array) { - if(this.sect_elem) { - for(let str of this.sect_elem.strings) { + if(this.sect.elem) { + for(let str of this.sect.elem.strings) { if(str.items.length > 0) { let COMP_PLUS_ELEM_ELEMENTS = new r.Array(this.file.COMP_PLUS_ELEM_ELEMENT, str.items.length); - file.set(COMP_PLUS_ELEM_ELEMENTS.toBuffer(str.items), str.offset + this.sect_elem._offset); + file.set(COMP_PLUS_ELEM_ELEMENTS.toBuffer(str.items), str.offset + this.sect.elem._offset); } } } diff --git a/common/web/types/src/kmx/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus.ts index aa04094b3a..1bb205333a 100644 --- a/common/web/types/src/kmx/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus.ts @@ -1,6 +1,7 @@ import { constants } from '@keymanapp/ldml-keyboard-constants'; import * as r from 'restructure'; import { ElementString } from './element-string.js'; +import { ListItem } from './string-list.js'; import { KMXFile } from './kmx.js'; @@ -15,6 +16,7 @@ export class GlobalSections { // These sections are used by other sections during compilation strs: Strs; elem: Elem; + list: List; } // 'sect' @@ -109,12 +111,28 @@ export class Ordr extends Section { // 'strs' +/** + * A string item in memory. This will be replaced with an index + * into the string table at finalization. + */ export class StrsItem { readonly value: string; constructor(value: string) { this.value = value; } -} + compareTo(o: StrsItem): number { + return StrsItem.binaryStringCompare(this.value, o.value); + } + static binaryStringCompare(a: string, b: string): number { + // https://tc39.es/ecma262/multipage/abstract-operations.html#sec-islessthan + if(typeof a != 'string' || typeof b != 'string') { + throw new Error('binaryStringCompare: inputs must be strings'); + } + if(a < b) return -1; + if(a > b) return 1; + return 0; + } +}; export class Strs extends Section { strings: StrsItem[] = [ new StrsItem('') ]; // C7043: The null string is always requierd @@ -190,6 +208,7 @@ export class Vkey extends Section { vkeys: VkeyItem[] = []; }; +// 'disp' export class DispItem { to: StrsItem; display: StrsItem; @@ -200,13 +219,132 @@ export class Disp extends Section { disps: DispItem[] = []; }; +// 'layr' + +/** + * In-memory `` + */ +export class LayrList { + flags: number; + hardware: StrsItem; + layers: LayrEntry[] = []; + minDeviceWidth: number; // millimeters +}; + +/** + * In-memory `` + */ + export class LayrEntry { + id: StrsItem; + modifier: StrsItem; + rows: LayrRow[] = []; +}; + +/** + * In-memory `` + */ + export class LayrRow { + keys: StrsItem[] = []; +}; + +export class Layr extends Section { + lists: LayrList[] = []; +}; + +export class Key2Keys { + flags: number; + flicks: string; // for in-memory only + id: StrsItem; + longPress: ListItem; + longPressDefault: StrsItem; + multiTap: ListItem; + switch: StrsItem; + to: StrsItem; + vkey: number; + width: number; +}; + +export class Key2Flicks { + flicks: Key2Flick[] = []; + id: StrsItem; + compareTo(b: Key2Flicks): number { + return this.id.compareTo(b.id); + } + constructor(id: StrsItem) { + this.id = id; + } +}; + +export class Key2Flick { + directions: ListItem; + flags: number; + to: StrsItem; +}; + +export class Key2 extends Section { + keys: Key2Keys[] = []; + flicks: Key2Flicks[] = []; + constructor(strs: Strs) { + super(); + let nullFlicks = new Key2Flicks(strs.allocString('')); + this.flicks.push(nullFlicks); // C7043: null element string + } +}; + +export class List extends Section { + /** + * Allocate a list from a space-separated list of items. + * Note that passing undefined or null or `''` will + * end up being the same as the empty list `[]` + * @param strs Strs section for allocation + * @param s space-separated list of items + * @returns a List object + */ + allocListFromSpaces(strs: Strs, s?: string): ListItem { + s = s ?? ''; + // TODO-LDML: support unicode escaping etc + return this.allocList(strs, s.split(' ')); + } + /** + * Return a List object referring to the string list. + * Note that a falsy list, or a list containing only an empty string + * `['']` will be stored as an empty list `[]`. + * @param strs Strs section for allocation + * @param s string list to allocate + * @returns + */ + allocList(strs: Strs, s?: string[]): ListItem { + // Special case the 'null' list for [] or [''] + if (!s || (s.length === 1 && s[0] === '')) { + return this.lists[0]; + } + let result = this.lists.find(item => item.isEqual(s)); + if(result === undefined) { + // allocate a new ListItem + result = new ListItem(strs, s); + this.lists.push(result); + } + return result; + } + constructor(strs: Strs) { + super(); + this.lists.push(new ListItem(strs, [])); // C7043: null element string + } + lists: ListItem[] = []; +}; + +export { ListItem as ListItem }; + export interface KMXPlusData { sect?: Strs; // sect is ignored in-memory bksp?: Bksp; disp?: Disp; elem?: Elem; // elem is ignored in-memory finl?: Finl; + key2?: Key2; keys?: Keys; + layr?: Layr; + list?: List; // list is ignored in-memory loca?: Loca; meta?: Meta; name?: Name; @@ -241,6 +379,21 @@ export class KMXPlusFile extends KMXFile { public readonly COMP_PLUS_KEYS_ITEM: any; public readonly COMP_PLUS_KEYS: any; + public readonly COMP_PLUS_LAYR_ENTRY: any; + public readonly COMP_PLUS_LAYR_KEY: any; + public readonly COMP_PLUS_LAYR_LIST: any; + public readonly COMP_PLUS_LAYR_ROW: any; + public readonly COMP_PLUS_LAYR: any; + + public readonly COMP_PLUS_KEY2_FLICK: any; + public readonly COMP_PLUS_KEY2_FLICKS: any; + public readonly COMP_PLUS_KEY2_KEY: any; + public readonly COMP_PLUS_KEY2: any; + + public readonly COMP_PLUS_LIST_LIST: any; + public readonly COMP_PLUS_LIST_INDEX: any; + public readonly COMP_PLUS_LIST: any; + public readonly COMP_PLUS_LOCA_ITEM: any; public readonly COMP_PLUS_LOCA: any; @@ -324,6 +477,7 @@ export class KMXPlusFile extends KMXFile { count: r.uint32le, reserved: new r.Reserved(r.uint32le), // padding strings: new r.Array(this.COMP_PLUS_ELEM_STRING, 'count') + // + variable subtable: Element data (see KMXPlusBuilder.emitElements()) }); // 'finl' - see 'tran' @@ -345,6 +499,106 @@ export class KMXPlusFile extends KMXFile { items: new r.Array(this.COMP_PLUS_KEYS_ITEM, 'count') }); + // 'layr' + + this.COMP_PLUS_LAYR_ENTRY = new r.Struct({ + id: r.uint32le, // str + modifier: r.uint32le, // str + row: r.uint32le, // index into rows + count: r.uint32le, + }); + + this.COMP_PLUS_LAYR_KEY = new r.Struct({ + key: r.uint32le, // str: key id + }); + + this.COMP_PLUS_LAYR_LIST = new r.Struct({ + flags: r.uint32le, + hardware: r.uint32le, //str + layer: r.uint32le, // index into layers + count: r.uint32le, + minDeviceWidth: r.uint32le, // integer: millimeters + }); + + this.COMP_PLUS_LAYR_ROW = new r.Struct({ + key: r.uint32le, + count: r.uint32le, + }); + + this.COMP_PLUS_LAYR = new r.Struct({ + ident: r.uint32le, + size: r.uint32le, + listCount: r.uint32le, + layerCount: r.uint32le, + rowCount: r.uint32le, + keyCount: r.uint32le, + reserved0: new r.Reserved(r.uint32le), + reserved1: new r.Reserved(r.uint32le), + lists: new r.Array(this.COMP_PLUS_LAYR_LIST, 'listCount'), + layers: new r.Array(this.COMP_PLUS_LAYR_ENTRY, 'layerCount'), + rows: new r.Array(this.COMP_PLUS_LAYR_ROW, 'rowCount'), + keys: new r.Array(this.COMP_PLUS_LAYR_KEY, 'keyCount'), + }); + + this.COMP_PLUS_KEY2_FLICK = new r.Struct({ + directions: r.uint32le, // list + flags: r.uint32le, + to: r.uint32le, // str | codepoint + }); + + this.COMP_PLUS_KEY2_FLICKS = new r.Struct({ + count: r.uint32le, + flick: r.uint32le, + id: r.uint32le, // str + }); + + this.COMP_PLUS_KEY2_KEY = new r.Struct({ + vkey: r.uint32le, + to: r.uint32le, // str | codepoint + flags: r.uint32le, + id: r.uint32le, // str + switch: r.uint32le, // str + width: r.uint32le, // width*10 ( 1 = 0.1 keys) + longPress: r.uint32le, // list index + longPressDefault: r.uint32le, // str + multiTap: r.uint32le, // list index + flicks: r.uint32le, // index into flicks table + }); + + this.COMP_PLUS_KEY2 = new r.Struct({ + ident: r.uint32le, + size: r.uint32le, + keyCount: r.uint32le, + flicksCount: r.uint32le, + flickCount: r.uint32le, + reserved0: new r.Reserved(r.uint32le), + reserved1: new r.Reserved(r.uint32le), + reserved2: new r.Reserved(r.uint32le), + keys: new r.Array(this.COMP_PLUS_KEY2_KEY, 'keyCount'), + flicks: new r.Array(this.COMP_PLUS_KEY2_FLICKS, 'flicksCount'), + flick: new r.Array(this.COMP_PLUS_KEY2_FLICK, 'flickCount'), + }); + + // 'list' + + this.COMP_PLUS_LIST_LIST = new r.Struct({ + index: r.uint32le, + count: r.uint32le, + }); + + this.COMP_PLUS_LIST_INDEX = new r.Struct({ + str: r.uint32le, // str + }); + + this.COMP_PLUS_LIST = new r.Struct({ + ident: r.uint32le, + size: r.uint32le, + listCount: r.uint32le, + indexCount: r.uint32le, + lists: new r.Array(this.COMP_PLUS_LIST_LIST, 'listCount'), + indices: new r.Array(this.COMP_PLUS_LIST_INDEX, 'indexCount'), + }); + // 'loca' this.COMP_PLUS_LOCA_ITEM = r.uint32le; //str @@ -413,6 +667,7 @@ export class KMXPlusFile extends KMXFile { count: r.uint32le, reserved: new r.Reserved(r.uint32le), // padding items: new r.Array(this.COMP_PLUS_STRS_ITEM, 'count') + // + variable subtable: String data (see KMXPlusBuilder.emitStrings()) }); // 'tran' diff --git a/common/web/types/src/kmx/string-list.ts b/common/web/types/src/kmx/string-list.ts new file mode 100644 index 0000000000..9805073ff9 --- /dev/null +++ b/common/web/types/src/kmx/string-list.ts @@ -0,0 +1,74 @@ +import { Strs, StrsItem } from './kmx-plus.js'; + +/** + * A single entry in a ListItem. + * Contains a StrsItem as its value. + */ +export class ListIndex { + readonly value: StrsItem; // will become index into Strs table + constructor(value: StrsItem) { + this.value = value; + } + isEqual(a: ListIndex | string) { + // so we can compare this to a string + return a.toString() === this.toString(); + } + toString(): string { + return this.value.value; + } +}; + +/** + * A string list in memory. This will be replaced with an index + * into the string table at finalization. + */ +export class ListItem extends Array { + /** + * Construct a new list from an array of strings. + * Use List. This is meant to be called by the List.allocString*() functions. + * @param strs the Strs section is needed to construct this object. + * @param source array of strings + * @returns + */ + constructor(strs: Strs, source: Array) { + super(); + if(!source) { + return; + } + + for (const str of source) { + let index = new ListIndex(strs.allocString(str)); + this.push(index); + } + } + isEqual(a: ListItem | string[]): boolean { + if (a.length != this.length) { + return false; + } + for (let i = 0; i < a.length; i++) { + if (!this[i].isEqual(a[i])) { + return false; + } + } + return true; + } + compareTo(o: ListItem): number { + for (let i = 0; i < Math.min(this.length, o.length); i++) { + const r = this[i].value.compareTo(o[i].value); + if (r !== 0) { + return r; + } + } + // prefix is the same, so go by length: shortest is first. + if (this.length < o.length) { + return -1; + } else if (this.length > o.length) { + return 1; + } else { + return 0; + } + } + toString(): string { + return this.map(v => v.value.value).toString(); + } +}; diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts index 6d34a8a309..33c30e8bfc 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts @@ -15,6 +15,7 @@ export default class LDMLKeyboardXMLSourceFileReader { boxXmlArray(source?.keyboard?.names, 'name'); boxXmlArray(source?.keyboard?.vkeys, 'vkey'); boxXmlArray(source?.keyboard?.keys, 'key'); + boxXmlArray(source?.keyboard?.keys, 'flicks'); boxXmlArray(source?.keyboard?.locales, 'locale'); boxXmlArray(source?.keyboard, 'transforms'); if(source?.keyboard?.layers) { @@ -27,6 +28,11 @@ export default class LDMLKeyboardXMLSourceFileReader { } } } + if(source?.keyboard?.keys?.flicks) { + for(let flicks of source?.keyboard?.keys?.flicks) { + boxXmlArray(flicks, 'flick'); + } + } if(source?.keyboard?.transforms) { for(let transform of source.keyboard.transforms) { boxXmlArray(transform, 'transform'); diff --git a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts index 568d186ae6..9f83ad17d5 100644 --- a/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts +++ b/common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts @@ -66,22 +66,51 @@ export interface LKSettings { export interface LKKeys { key: LKKey[]; + flicks: LKFlicks[]; }; export interface LKKey { id?: string; + flicks?: string; to?: string; gap?: boolean; switch?: string; + longPress?: string; + longPressDefault?: string; + multiTap?: string; + transform?: "no"; + width?: number; +}; + +export interface LKFlicks { + id?: string; + flick?: LKFlick[]; +}; + +export interface LKFlick { + directions?: string; + to?: string; }; export interface LKLayers { + /** + * `hardware` or `touch` + */ form?: string; + /** + * `us`, `iso`, `jis`, or `abnt2` + */ + hardware?: string; + /** + * Minimum width in millimeters + */ + minDeviceWidth?: number; layer?: LKLayer[]; }; export interface LKLayer { id?: string; + modifier?: string; row?: LKRow[]; }; diff --git a/core/include/ldml/keyboardprocessor_ldml.h b/core/include/ldml/keyboardprocessor_ldml.h index aa5db68abc..ce319c8176 100644 --- a/core/include/ldml/keyboardprocessor_ldml.h +++ b/core/include/ldml/keyboardprocessor_ldml.h @@ -24,7 +24,14 @@ #define LDML_ELEM_FLAGS_TERTIARY_MASK 0xFF000000 #define LDML_ELEM_FLAGS_UNICODE_SET 0x1 #define LDML_FINL_FLAGS_ERROR 0x1 +#define LDML_KEY2_FLICK_FLAGS_EXTEND 0x1 +#define LDML_KEY2_KEY_FLAGS_EXTEND 0x1 +#define LDML_KEY2_KEY_FLAGS_GAP 0x2 +#define LDML_KEY2_KEY_FLAGS_NOTRANSFORM 0x4 #define LDML_KEYS_FLAGS_EXTEND 0x1 +#define LDML_LAYR_LIST_FLAGS_HARDWARE 0x0 +#define LDML_LAYR_LIST_FLAGS_MASK_FORM 0x1 +#define LDML_LAYR_LIST_FLAGS_TOUCH 0x1 #define LDML_LENGTH_BKSP 0x10 #define LDML_LENGTH_BKSP_ITEM 0x10 #define LDML_LENGTH_DISP 0x20 @@ -35,8 +42,20 @@ #define LDML_LENGTH_FINL 0x10 #define LDML_LENGTH_FINL_ITEM 0x10 #define LDML_LENGTH_HEADER 0x8 +#define LDML_LENGTH_KEY2 0x20 +#define LDML_LENGTH_KEY2_FLICK_ELEMENT 0xC +#define LDML_LENGTH_KEY2_FLICK_LIST 0xC +#define LDML_LENGTH_KEY2_KEY 0x28 #define LDML_LENGTH_KEYS 0x10 #define LDML_LENGTH_KEYS_ITEM 0x10 +#define LDML_LENGTH_LAYR 0x20 +#define LDML_LENGTH_LAYR_ENTRY 0x10 +#define LDML_LENGTH_LAYR_KEY 0x4 +#define LDML_LENGTH_LAYR_LIST 0x14 +#define LDML_LENGTH_LAYR_ROW 0x8 +#define LDML_LENGTH_LIST 0x10 +#define LDML_LENGTH_LIST_INDEX 0x4 +#define LDML_LENGTH_LIST_ITEM 0x8 #define LDML_LENGTH_LOCA 0x10 #define LDML_LENGTH_LOCA_ITEM 0x4 #define LDML_LENGTH_META 0x24 @@ -63,8 +82,14 @@ #define LDML_SECTIONNAME_ELEM "elem" #define LDML_SECTIONID_FINL 0x6C6E6966 /* "finl" */ #define LDML_SECTIONNAME_FINL "finl" +#define LDML_SECTIONID_KEY2 0x3279656B /* "key2" */ +#define LDML_SECTIONNAME_KEY2 "key2" #define LDML_SECTIONID_KEYS 0x7379656B /* "keys" */ #define LDML_SECTIONNAME_KEYS "keys" +#define LDML_SECTIONID_LAYR 0x7279616C /* "layr" */ +#define LDML_SECTIONNAME_LAYR "layr" +#define LDML_SECTIONID_LIST 0x7473696C /* "list" */ +#define LDML_SECTIONNAME_LIST "list" #define LDML_SECTIONID_LOCA 0x61636F6C /* "loca" */ #define LDML_SECTIONNAME_LOCA "loca" #define LDML_SECTIONID_META 0x6174656D /* "meta" */ diff --git a/core/include/ldml/keyboardprocessor_ldml.ts b/core/include/ldml/keyboardprocessor_ldml.ts index 9404a4e668..1d3513860a 100644 --- a/core/include/ldml/keyboardprocessor_ldml.ts +++ b/core/include/ldml/keyboardprocessor_ldml.ts @@ -23,12 +23,16 @@ * through. */ export type SectionIdent = +// Keep this sorted, but with `sect` as the first entry. 'sect' | 'bksp' | 'disp' | 'elem' | 'finl' | + 'key2' | 'keys' | + 'layr' | + 'list' | 'loca' | 'meta' | 'name' | @@ -226,9 +230,104 @@ class Constants { readonly keys_flags_extend = 1; /* ------------------------------------------------------------------ - * loca section + * key2 section + ------------------------------------------------------------------ */ + + /** + * Minimum length of the 'key2' section not including variable parts + */ + readonly length_key2 = 32; + /** + * Length of each item in the 'key2' keys sub-table + */ + readonly length_key2_key = 40; + /** + * Length of each item in the 'key2' flick lists sub-table + */ + readonly length_key2_flick_list = 12; + /** + * Length of each item in the 'key2' flick elements sub-table + */ + readonly length_key2_flick_element = 12; + + /** + * 0 if to is a char, 1 if it is a string + */ + readonly key2_key_flags_extend = 0x00000001; + + /** + * 1 if the key is a gap + */ + readonly key2_key_flags_gap = 0x00000002; + + /** + * 1 if the key is transform=no + */ + readonly key2_key_flags_notransform = 0x00000004; + + /** + * 0 if to is a char, 1 if it is a string + */ + readonly key2_flick_flags_extend = 0x00000001; + + /* ------------------------------------------------------------------ + * layr section + ------------------------------------------------------------------ */ + + /** + * Minimum length of the 'layr' section not including variable parts + */ + readonly length_layr = 32; + /** + * Length of each layer list in the 'layr' section variable part + */ + readonly length_layr_list = 20; + /** + * bitmask for the 'form' field of the layr.list[].flags bitfield + */ + readonly layr_list_flags_mask_form = 1; + /** + * hardware layout: value for the 'form' field of the layr.list[].flags + */ + readonly layr_list_flags_hardware = 0; + /** + * touch layout: value for the 'form' field of the layr.list[].flags + */ + readonly layr_list_flags_touch = 1; + /** + * Length of each layer entry in the 'layr' section variable part + */ + readonly length_layr_entry = 16; + /** + * Length of each row entry in the 'layr' section variable part + */ + readonly length_layr_row = 8; + /** + * Length of each key entry in the 'layr' section variable part + */ + readonly length_layr_key = 4; + + /* ------------------------------------------------------------------ + * list section ------------------------------------------------------------------ */ + /** + * Minimum length of the 'list' section not including variable parts + */ + readonly length_list = 16; + /** + * Length of each list item in the 'list' list section variable part + */ + readonly length_list_item = 8; + /** + * Length of each list item in the 'list' indices section variable part + */ + readonly length_list_index = 4; + + /* ------------------------------------------------------------------ + * loca section + ------------------------------------------------------------------ */ + /** * Minimum length of the 'loca' section not including variable parts */ @@ -332,11 +431,15 @@ class Constants { * All section IDs. */ readonly section: SectionMap = { + // keep this sorted bksp: 'bksp', disp: 'disp', elem: 'elem', finl: 'finl', + key2: 'key2', keys: 'keys', + layr: 'layr', + list: 'list', loca: 'loca', meta: 'meta', name: 'name', @@ -353,8 +456,8 @@ class Constants { * @returns hex ID such as 0x74636573 */ hex_section_id(id:string) { - if(!id || typeof id !== 'string' || !id.match(/[a-z][a-z][a-z][a-z]/)) { - throw Error(`hex_section_id(${id}) - need a 4-character string`); + if(!id || typeof id !== 'string' || !id.match(/^[a-z0-9]{4}$/)) { + throw Error(`hex_section_id(${id}) - need a 4-character alphanumeric lower-case string`); } let r = 0; for (let i = 3; i>=0; i--) { diff --git a/core/src/kmx/kmx_plus.cpp b/core/src/kmx/kmx_plus.cpp index 03bbb0e559..6a39ecadb3 100644 --- a/core/src/kmx/kmx_plus.cpp +++ b/core/src/kmx/kmx_plus.cpp @@ -352,6 +352,148 @@ COMP_KMXPLUS_TRAN::valid(KMX_DWORD _kmn_unused(length)) const { return true; } +bool +COMP_KMXPLUS_LAYR::valid(KMX_DWORD _kmn_unused(length)) const { + if (header.size < sizeof(*this) + + (listCount * sizeof(COMP_KMXPLUS_LAYR_LIST)) + + (layerCount * sizeof(COMP_KMXPLUS_LAYR_ENTRY)) + + (rowCount * sizeof(COMP_KMXPLUS_LAYR_ROW)) + + (keyCount * sizeof(COMP_KMXPLUS_LAYR_KEY))) { + DebugLog("header.size < expected size"); + return false; + } + // TODO-LDML + DebugLog("!! More to do here."); + return true; +} + +COMP_KMXPLUS_LAYR_Helper::COMP_KMXPLUS_LAYR_Helper() : layr(nullptr), is_valid(false) { +} + +bool +COMP_KMXPLUS_LAYR_Helper::setLayr(const COMP_KMXPLUS_LAYR *newLayr) { + is_valid = true; + if (newLayr == nullptr) { + // null = invalid + is_valid = false; + return false; + } + layr = newLayr; + const uint8_t *rawdata = reinterpret_cast(this); + rawdata += LDML_LENGTH_LAYR; // skip past non-dynamic portion + // lists + if (layr->listCount > 0) { + lists = reinterpret_cast(rawdata); + } else { + lists = nullptr; + is_valid = false; + } + rawdata += sizeof(COMP_KMXPLUS_LAYR_LIST) * layr->listCount; + // entries + if (layr->layerCount > 0) { + entries = reinterpret_cast(rawdata); + } else { + entries = nullptr; + is_valid = false; + } + rawdata += sizeof(COMP_KMXPLUS_LAYR_ENTRY) * layr->layerCount; + // rows + if (layr->rowCount > 0) { + rows = reinterpret_cast(rawdata); + } else { + rows = nullptr; + is_valid = false; + } + rawdata += sizeof(COMP_KMXPLUS_LAYR_ROW) * layr->rowCount; + // keys + if (layr->keyCount > 0) { + keys = reinterpret_cast(rawdata); + } else { + keys = nullptr; + is_valid = false; + } + + // Now, validate offsets by walking + if (is_valid) { + for(KMX_DWORD i = 0; is_valid && i < layr->listCount; i++) { + const COMP_KMXPLUS_LAYR_LIST &list = lists[i]; + // is the count off the end? + if ((list.layer >= layr->layerCount) || (list.layer + list.count > layr->layerCount)) { + DebugLog("COMP_KMXPLUS_LAYR_Helper: list[%d] would access layer %d+%d, > count %d", + i, list.layer, list.count, layr->layerCount); + is_valid = false; + } + } + for(KMX_DWORD i = 0; is_valid && i < layr->layerCount; i++) { + const COMP_KMXPLUS_LAYR_ENTRY &entry = entries[i]; + // is the count off the end? + if ((entry.row >= layr->rowCount) || (entry.row + entry.count > layr->rowCount)) { + DebugLog("COMP_KMXPLUS_LAYR_Helper: entry[%d] would access row %d+%d, > count %d", + i, entry.row, entry.count, layr->rowCount); + is_valid = false; + } + } + for(KMX_DWORD i = 0; is_valid && i < layr->rowCount; i++) { + const COMP_KMXPLUS_LAYR_ROW &row = rows[i]; + // is the count off the end? + if ((row.key >= layr->keyCount) || (row.key + row.count > layr->keyCount)) { + DebugLog("COMP_KMXPLUS_LAYR_Helper: row[%d] would access key %d+%d, > count %d", + i, row.key, row.count, layr->keyCount); + is_valid = false; + } + } + } + // Return results + DebugLog("COMP_KMXPLUS_LAYR_Helper.setLayr(): %s", is_valid ? "valid" : "invalid"); + return is_valid; +} + +bool COMP_KMXPLUS_LAYR_Helper::valid() const { + return is_valid; +} + +const COMP_KMXPLUS_LAYR_LIST * +COMP_KMXPLUS_LAYR_Helper::getList(KMX_DWORD list) const { + if (!valid() || list >= layr->listCount) + return nullptr; + return lists + list; +} + +const COMP_KMXPLUS_LAYR_ENTRY * +COMP_KMXPLUS_LAYR_Helper::getEntry(KMX_DWORD entry) const { + if (!valid() || entry >= layr->layerCount) + return nullptr; + return entries + entry; +} + +const COMP_KMXPLUS_LAYR_ROW * +COMP_KMXPLUS_LAYR_Helper::getRow(KMX_DWORD row) const { + if (!valid() || row >= layr->rowCount) + return nullptr; + return rows + row; +} + +const COMP_KMXPLUS_LAYR_KEY * +COMP_KMXPLUS_LAYR_Helper::getKey(KMX_DWORD key) const { + if (!valid() || key >= layr->keyCount) + return nullptr; + return keys + key; +} + +bool +COMP_KMXPLUS_KEY2::valid(KMX_DWORD _kmn_unused(length)) const { + // TODO-LDML more to do here + DebugLog("TODO-LDML: key2"); + return true; +} + +bool +COMP_KMXPLUS_LIST::valid(KMX_DWORD _kmn_unused(length)) const { + // TODO-LDML more to do here + DebugLog("TODO-LDML: list"); + return true; +} + // ---- constructor kmx_plus::kmx_plus(const COMP_KEYBOARD *keyboard, size_t length) @@ -390,12 +532,18 @@ kmx_plus::kmx_plus(const COMP_KEYBOARD *keyboard, size_t length) // these will be nullptr if they don't validate disp = section_from_sect(sect); elem = section_from_sect(sect); + key2 = section_from_sect(sect); keys = section_from_sect(sect); + layr = section_from_sect(sect); + list = section_from_sect(sect); loca = section_from_sect(sect); meta = section_from_sect(sect); strs = section_from_sect(sect); tran = section_from_sect(sect); vkey = section_from_sect(sect); + + // calculate and validate the layer dynamic parts + (void)layrHelper.setLayr(layr); } } diff --git a/core/src/kmx/kmx_plus.h b/core/src/kmx/kmx_plus.h index c5ebb94fab..b15fad77f1 100644 --- a/core/src/kmx/kmx_plus.h +++ b/core/src/kmx/kmx_plus.h @@ -371,8 +371,143 @@ struct COMP_KMXPLUS_DISP { static_assert(sizeof(struct COMP_KMXPLUS_DISP) % 0x10 == 0, "Structs prior to entries[] should align to 128-bit boundary"); static_assert(sizeof(struct COMP_KMXPLUS_DISP) == LDML_LENGTH_DISP, "mismatched size of section disp"); + + +/* ------------------------------------------------------------------ + * layr section + ------------------------------------------------------------------ */ + +struct COMP_KMXPLUS_LAYR_LIST { + KMX_DWORD flags; + KMXPLUS_STR hardware; + KMX_DWORD layer; + KMX_DWORD count; + KMX_DWORD minDeviceWidth; +}; + +static_assert(sizeof(struct COMP_KMXPLUS_LAYR_LIST) == LDML_LENGTH_LAYR_LIST, "mismatched size of COMP_KMXPLUS_LAYR_LIST"); + +struct COMP_KMXPLUS_LAYR_ENTRY { + KMXPLUS_STR id; + KMXPLUS_STR modifier; + KMX_DWORD row; + KMX_DWORD count; +}; + +static_assert(sizeof(struct COMP_KMXPLUS_LAYR_ENTRY) == LDML_LENGTH_LAYR_ENTRY, "mismatched size of COMP_KMXPLUS_LAYR_ENTRY"); + +struct COMP_KMXPLUS_LAYR_ROW { + KMX_DWORD key; + KMX_DWORD count; +}; + +static_assert(sizeof(struct COMP_KMXPLUS_LAYR_ROW) == LDML_LENGTH_LAYR_ROW, "mismatched size of COMP_KMXPLUS_LAYR_ROW"); + + +struct COMP_KMXPLUS_LAYR_KEY { + KMX_DWORD key; // index into key2 section +}; + +static_assert(sizeof(struct COMP_KMXPLUS_LAYR_KEY) == LDML_LENGTH_LAYR_KEY, "mismatched size of COMP_KMXPLUS_LAYR_KEY"); + +struct COMP_KMXPLUS_LAYR { + static const KMX_DWORD IDENT = LDML_SECTIONID_LAYR; + COMP_KMXPLUS_HEADER header; + KMX_DWORD listCount; + KMX_DWORD layerCount; + KMX_DWORD rowCount; + KMX_DWORD keyCount; + KMX_DWORD reserved[2]; + // entries, rows, and keys have a dynamic offset + // use COMP_KMXPLUS_LAYR_Helper to access. + // + // COMP_KMXPLUS_LAYR_LIST lists[]; + // COMP_KMXPLUS_LAYR_ENTRY entries[]; + // COMP_KMXPLUS_LAYR_ROW rows[]; + // COMP_KMXPLUS_LAYR_KEY keys[]; + /** + * @brief True if section is valid. + */ + bool valid(KMX_DWORD length) const; +}; + /** - * @brief helper accessor object for + * Helper accessor for the dynamic part of a layr section. + */ +class COMP_KMXPLUS_LAYR_Helper { +public: + COMP_KMXPLUS_LAYR_Helper(); + /** + * Initialize the helper to point at a layr section. + * @return true if valid + */ + bool setLayr(const COMP_KMXPLUS_LAYR *newLayr); + bool valid() const; + + const COMP_KMXPLUS_LAYR_LIST *getList(KMX_DWORD list) const; + const COMP_KMXPLUS_LAYR_ENTRY *getEntry(KMX_DWORD entry) const; + const COMP_KMXPLUS_LAYR_ROW *getRow(KMX_DWORD row) const; + const COMP_KMXPLUS_LAYR_KEY *getKey(KMX_DWORD key) const; + +private: + const COMP_KMXPLUS_LAYR *layr; + bool is_valid; + const COMP_KMXPLUS_LAYR_LIST *lists; + const COMP_KMXPLUS_LAYR_ENTRY *entries; + const COMP_KMXPLUS_LAYR_ROW *rows; + const COMP_KMXPLUS_LAYR_KEY *keys; +}; + +static_assert(sizeof(struct COMP_KMXPLUS_LAYR) % 0x10 == 0, "Structs prior to entries[] should align to 128-bit boundary"); +static_assert(sizeof(struct COMP_KMXPLUS_LAYR) == LDML_LENGTH_LAYR, "mismatched size of section layr"); + +/* ------------------------------------------------------------------ + * key2 section + ------------------------------------------------------------------ */ +struct COMP_KMXPLUS_KEY2 { + static const KMX_DWORD IDENT = LDML_SECTIONID_KEY2; + COMP_KMXPLUS_HEADER header; + KMX_DWORD keyCount; + KMX_DWORD flicksCount; + KMX_DWORD flickCount; + KMX_DWORD reserved[3]; + // TODO-LDML: keys sub-table + // TODO-LDML: flick lists sub-table + // TODO-LDML: flick elements sub-table + + /** + * @brief True if section is valid. + */ + bool valid(KMX_DWORD length) const; +}; + +static_assert(sizeof(struct COMP_KMXPLUS_KEY2) % 0x10 == 0, "Structs prior to entries[] should align to 128-bit boundary"); +static_assert(sizeof(struct COMP_KMXPLUS_KEY2) == LDML_LENGTH_KEY2, "mismatched size of section key2"); + + +/* ------------------------------------------------------------------ + * list section + ------------------------------------------------------------------ */ +struct COMP_KMXPLUS_LIST { + static const KMX_DWORD IDENT = LDML_SECTIONID_LIST; + COMP_KMXPLUS_HEADER header; + KMX_DWORD listCount; + KMX_DWORD indexCount; + // TODO-LDML: lists sub-table + // TODO-LDML: indices sub-table + + /** + * @brief True if section is valid. + */ + bool valid(KMX_DWORD length) const; +}; + +static_assert(sizeof(struct COMP_KMXPLUS_LIST) % 0x10 == 0, "Structs prior to entries[] should align to 128-bit boundary"); +static_assert(sizeof(struct COMP_KMXPLUS_LIST) == LDML_LENGTH_LIST, "mismatched size of section list"); + + +/** + * @brief helper accessor object for KMX Plus data * */ class kmx_plus { @@ -386,9 +521,13 @@ class kmx_plus { * @param length length of the entire KMX file */ kmx_plus(const COMP_KEYBOARD *keyboard, size_t length); + // keep the next elements sorted const COMP_KMXPLUS_DISP *disp; const COMP_KMXPLUS_ELEM *elem; + const COMP_KMXPLUS_KEY2 *key2; const COMP_KMXPLUS_KEYS *keys; + const COMP_KMXPLUS_LAYR *layr; + const COMP_KMXPLUS_LIST *list; const COMP_KMXPLUS_LOCA *loca; const COMP_KMXPLUS_META *meta; const COMP_KMXPLUS_SECT *sect; @@ -398,6 +537,7 @@ class kmx_plus { inline bool is_valid() { return valid; } private: bool valid; // true if valid + COMP_KMXPLUS_LAYR_Helper layrHelper; }; /** diff --git a/core/src/ldml/C7043_ldml.md b/core/src/ldml/C7043_ldml.md index 065899b4dd..3bfc9d62b9 100644 --- a/core/src/ldml/C7043_ldml.md +++ b/core/src/ldml/C7043_ldml.md @@ -338,19 +338,23 @@ Represents layers on the keyboard. |16 | 32 | rowCount | int: number of row entries | |20 | 32 | keyCount | int: number of key entries | |24 | 64 | reserved | padding | -|32 | var | layers | layers sub-table | +|32 | var | lists | layer list sub-table | +| - | var | layers | layers sub-table | | - | var | rows | rows sub-table | | - | var | keys | keys sub-table | +### `layr.lists` subtable + Each layer list corresponds to one `` element. There are `listCount` total lists. -| ∆ | Bits | Name | Description | -|---|------|------------|--------------------------------------------| -| 0+| 32 | flags | int: per-layers options | -| 4+| 32 | hardware | str: layout (`us`,`iso`,`jis`,`abnt2`) | -| 8+| 32 | layer | int: index to first layer element | -|12+| 32 | count | int: number of layer elements in this list | +| ∆ | Bits | Name | Description | +|---|------|------------------|--------------------------------------------| +| 0+| 32 | flags | int: per-layers options | +| 4+| 32 | hardware | str: layout (`us`,`iso`,`jis`,`abnt2`) | +| 8+| 32 | layer | int: index to first layer element | +|12+| 32 | count | int: number of layer elements in this list | +|16+| 32 | minDeviceWidth | int: min device width in millimeters, or 0 | - `flags`: a 32-bit bitfield defined as below: @@ -359,6 +363,8 @@ There are `listCount` total lists. | 0 | form | 0: hardware  | | 0 | form | 1: touch | +Layers are sorted hardware-first, then by minimum width ascending. + ### `layr.layers` subtable Each layer entry corresponds to one `` element @@ -388,7 +394,7 @@ There are `keyCount` total key entries. | ∆ | Bits | Name | Description | |---|------|---------|------------------------------------------| -| 0+| 32 | key | int: index into `key2` section | +| 0+| 32 | key | str: key id | ### C7043.2.14 `disp`—Display list @@ -419,8 +425,9 @@ Entries are sorted in a binary codepoint sort on the `to` field. | 4 | 32 | size | int: Length of section | | 8 | 32 | keyCount | int: Number of keys | |12 | 32 | flicksCount | int: Number of flick lists | -|12 | 32 | flickCount | int: Number of flick elements | -|16 | var | keys | keys sub-table | +|16 | 32 | flickCount | int: Number of flick elements | +|20 | 96 | reserved | padding | +|32 | var | keys | keys sub-table | | - | var | flicks | flick lists sub-table | | - | var | flick | flick elements sub-table | @@ -471,6 +478,7 @@ For each flicks in the flick list: Elements are ordered by the string id. +If this section is present, it must have a 'flicks' in the list at position zero with count=0, index=0 and id=0 meaning 'no flicks'. #### `key2.flick` flick element subtable For each flick element: @@ -479,12 +487,21 @@ For each flick element: |---|------|---------------- |----------------------------------------------------------| | 0+| 32 | directions | list: index into `list` section with direction list | | 8+| 32 | flags | int: per-key flags | -|12+| 32 | to | str: output string | +|12+| 32 | to | str: output string, or ucs32: output char, see flags | + +If this section is present, it must have a 'flick element' at position zero with directions=0, flags=0, and to=0 meaning 'no flick'. There is not a 'null' flick element at the end of each list. Elements are ordered by the `flicks.id`, and secondarily by the directions list id. +- `flags`: Flags is a 32-bit bitfield defined as below: + +| Bit position | Meaning | Description | +|--------------|-----------|---------------------------------------------| +| 0 | extend | 0: `to` is a char, 1: `to` is a string | + + ### C7043.2.16 `list`—String lists | ∆ | Bits | Name | Description | @@ -493,7 +510,7 @@ Elements are ordered by the `flicks.id`, and secondarily by the directions list | 4 | 32 | size | int: Length of section | | 8 | 32 | listCount | int: Total number of lists elements | |12 | 32 | indexCount | int: Total number of index elements | -|32 | var | lists | list sub-table | +|16 | var | lists | list sub-table | | - | var | indices | index sub-table | #### `list.lists` sub-table diff --git a/core/src/ldml/C7532_ldml_updating.md b/core/src/ldml/C7532_ldml_updating.md new file mode 100644 index 0000000000..d5c75188b0 --- /dev/null +++ b/core/src/ldml/C7532_ldml_updating.md @@ -0,0 +1,76 @@ +# How to update KMXPlus sections + +A diary. +By Steven R. Loomis + +Keyman Section Update Journal + +working on ‘layr’, using ‘disp’ as a model from https://github.com/keymanapp/keyman/pull/7568 + +## Constants and Scaffolding + +- *Edit/Commit*: `core/include/ldml/keyboardprocessor_ldml.ts` + - update `SectionIdent` and keep in order + - update `SectionMap` and keep in order + - add a comment block in order `layr section` + - add `length_layr` with the nonvariable length + - add a `length_layr_*` for each subitem + - add parameters for each flag/bitfield + - Check indentation, check for copypasta errs! +- Run: `./core/tools/ldml-const-builder/build.sh clean build run` +- Verify/Commit: `core/include/ldml/keyboardprocessor_ldml.h` + +## XML changes + +- `resources/standards-data/ldml-keyboards/techpreview/` : update / reimport / fix fixup script if needed +- E/C: `common/web/types/src/ldml-keyboard/ldml-keyboard-xml.ts` + - add to `LKKeyboard` and subproperties as needed to support the structure on the XML side +- Now would be a good time to stop and make sure everything compiles. It didn’t, there was an unrelated issue with snprintf! + +## In-memory data: Phase 1 + +- `common/web/types/src/kmx/kmx-plus.ts` + - update `KMXPlusData` to include new section + - add the new section and any in-memory data for the compiler + - It’s enough temporarily to add `export class Sect extends Section{ /* TODO-LDML */};` for now so that it compiles, and come back to it +- `core/src/kmx/kmx_plus.h` + - add new structs +- `core/src/kmx/kmx_plus.cpp` + - add validate implementation for the section and any new structs + - update `kmx_plus::kmx_plus()` to include the loader +- `common/web/types/src/kmx/kmx-plus.ts` + - Also update class KMXPlusFile to include the actual binary format (coordinate with `kmx_plus.h`) +- E/C `common/web/types/src/ldml-keyboard/ldml-keyboard-xml-reader.ts` if there’s any changing to the boxing needed + +## In-memory data: Phase 2 + +- first the compiler tests + - add a section in `developer/src/kmc-keyboard/test/fixtures/sections` if needed + - add a test case such as `developer/src/kmc-keyboard/test/test-key2.ts` +- add a compiler + - `developer/src/kmc-keyboard/compiler/key2.ts` + - link it in to `developer/src/kmc-keyboard/src/compiler/compiler.ts` + - add the import + - add to `SECTION_COMPILERS` + + - Note: the in-memory compiler can affect `basic.kmx` even _before_ you add the section writing code. How? Simple… `Strs.allocString()` is called for the in-memory structures, so the string table will start growing even before those strings are actually used by the new sections. This is why it's fine to ignore the basic failure until you actually do the KMXPlus write. + +## Writing out + +- The moment you've been waiting for! Crack open `common/web/types/src/kmx/kmx-plus-builder/kmx-plus-builder.ts` and do it. + - Add `import { BUILDER_DISP, build_disp } from './build-disp.js';` to the top (and a new file to go with it) — and, in order + - Add `private sect_disp: BUILDER_DISP` to `class KMXPlusBuilder {` — and, in order + - add `this.sect_disp = build_disp(this.file.kmxplus, this.sect_strs);` to the `build()` function. Include any other sections that need to be cross referenced. + - Update `finalize_sect` and add `offset = this.finalize_sect_item(this.sect_disp, offset);` + - Finally, add `this.emitSection(file, this.file.COMP_PLUS_DISP, this.sect_disp);` to `compile()` — and, in order. + - Note that some variable length parts (such as the actual text data in `strs`) are sometimes in a separate emit function. Anything that's not in the `COMP_PLUS_STRS` `r.Struct` definition needs one of these. + - Also note that restructure will happily ignore (write zeros for) any fields where the BUILDER_* fields don't match the COMP_PLUS_* fields. + +- update basic.xml and basic.txt + - Tweak `eveloper/src/kmc-keyboard/test/fixtures/basic.xml` as needed + - You can use `developer/src/kmc-keyboard/build.sh build-fixtures` which will generate these. The two .kmx files are supposed to match: if not, fix `basic.txt` or fix other bugs. + - `developer/src/kmc-keyboard/build/test/fixtures/basic-txt.kmx` - KMX generated from basic.txt. + - `developer/src/kmc-keyboard/build/test/fixtures/basic-xml.kmx` - KMX generated from basic.xml. + - `developer/src/kmc-keyboard/build/test/fixtures/basic-xml.kvk` - KVK generated from basic.xml. + +## more to come diff --git a/developer/src/kmc-keyboard/src/compiler/compiler.ts b/developer/src/kmc-keyboard/src/compiler/compiler.ts index 092403b27a..d176ab6dad 100644 --- a/developer/src/kmc-keyboard/src/compiler/compiler.ts +++ b/developer/src/kmc-keyboard/src/compiler/compiler.ts @@ -1,14 +1,16 @@ import { LDMLKeyboardXMLSourceFileReader, LDMLKeyboard, KMXPlus } from '@keymanapp/common-types'; import CompilerCallbacks from './callbacks.js'; import CompilerOptions from './compiler-options.js'; -import { DispCompiler } from './disp.js'; -import { KeysCompiler } from './keys.js'; -import { LocaCompiler } from './loca.js'; import { CompilerMessages } from './messages.js'; +import { BkspCompiler, FinlCompiler, TranCompiler } from './tran.js'; +import { DispCompiler } from './disp.js'; +import { Key2Compiler } from './key2.js'; +import { KeysCompiler } from './keys.js'; +import { LayrCompiler } from './layr.js'; +import { LocaCompiler } from './loca.js'; import { MetaCompiler } from './meta.js'; import { NameCompiler } from './name.js'; import { OrdrCompiler } from './ordr.js'; -import { BkspCompiler, FinlCompiler, TranCompiler } from './tran.js'; import { VkeyCompiler } from './vkey.js'; import LDMLKeyboardXMLSourceFile = LDMLKeyboard.LDMLKeyboardXMLSourceFile; @@ -17,14 +19,16 @@ import KMXPlusFile = KMXPlus.KMXPlusFile; const SECTION_COMPILERS = [ BkspCompiler, DispCompiler, - KeysCompiler, FinlCompiler, + Key2Compiler, + KeysCompiler, + LayrCompiler, LocaCompiler, MetaCompiler, NameCompiler, OrdrCompiler, TranCompiler, - VkeyCompiler + VkeyCompiler, ]; export default class Compiler { @@ -95,6 +99,7 @@ export default class Compiler { // These two sections are required by other sections kmx.kmxplus.strs = new KMXPlus.Strs(); kmx.kmxplus.elem = new KMXPlus.Elem(kmx.kmxplus.strs); + kmx.kmxplus.list = new KMXPlus.List(kmx.kmxplus.strs); for(let section of sections) { if(!section.validate()) { @@ -103,7 +108,7 @@ export default class Compiler { // errors for the keyboard developer. continue; } - const sect = section.compile({strs: kmx.kmxplus.strs, elem: kmx.kmxplus.elem}); + const sect = section.compile({strs: kmx.kmxplus.strs, elem: kmx.kmxplus.elem, list: kmx.kmxplus.list}); /* istanbul ignore if */ if(!sect) { diff --git a/developer/src/kmc-keyboard/src/compiler/disp.ts b/developer/src/kmc-keyboard/src/compiler/disp.ts index 3f3260447a..5eb7defa80 100644 --- a/developer/src/kmc-keyboard/src/compiler/disp.ts +++ b/developer/src/kmc-keyboard/src/compiler/disp.ts @@ -44,18 +44,7 @@ export class DispCompiler extends SectionCompiler { display: sections.strs.allocString(display.display), })) || []; - // TODO-LDML: Same function in comon/web/types/src - function binaryStringCompare(a: string, b: string) : number { - if(a < b) { - return -1; - } else if(a > b) { - return 1; - } else { - return 0; - } - } - - result.disps.sort((a: DispItem, b: DispItem) => binaryStringCompare(a.to.value, b.to.value)); + result.disps.sort((a: DispItem, b: DispItem) => a.to.compareTo(b.to)); return result; } diff --git a/developer/src/kmc-keyboard/src/compiler/key2.ts b/developer/src/kmc-keyboard/src/compiler/key2.ts new file mode 100644 index 0000000000..f1f9680428 --- /dev/null +++ b/developer/src/kmc-keyboard/src/compiler/key2.ts @@ -0,0 +1,96 @@ +import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { KMXPlus } from '@keymanapp/common-types'; +import { SectionCompiler } from "./section-compiler.js"; + +import GlobalSections = KMXPlus.GlobalSections; +import Key2 = KMXPlus.Key2; +import ListItem = KMXPlus.ListItem; +import Key2Flicks = KMXPlus.Key2Flicks; + + +export class Key2Compiler extends SectionCompiler { + + public get id() { + return constants.section.key2; + } + + public validate() { + let valid = true; + // TODO-LDML: some validation needed here? + return valid; + } + + + public compile(sections: GlobalSections): Key2 { + if (!this.keyboard.keys.key && !this.keyboard.keys.flicks) { + // short-circuit if no keys or flicks + return null; + } + + let sect = new Key2(sections.strs); + + // Load the flicks first + this.loadFlicks(sections, sect); + + // Now, load the keys + this.loadKeys(sections, sect); + + return sect; + } + + public loadFlicks(sections: GlobalSections, sect: Key2) { + for (let lkflicks of this.keyboard.keys.flicks) { + let flicks: Key2Flicks = new Key2Flicks(sections.strs.allocString(lkflicks.id)); + + for (let lkflick of lkflicks.flick) { + let flags = 0; + // TODO-LDML: single char + const to = sections.strs.allocString(lkflick.to); + flags |= constants.key2_flick_flags_extend; + let directions : ListItem = sections.list.allocListFromSpaces(sections.strs, lkflick.directions); + flicks.flicks.push({ + directions, + flags, + to, + }); + } + + sect.flicks.push(flicks); + } + } + + public loadKeys(sections: GlobalSections, sect: Key2) { + for (let key of this.keyboard.keys.key) { + let flags = 0; + const flicks = key.flicks; + // TODO-LDML: verify that this flick id exists + if (!!key.gap) { + flags |= constants.key2_key_flags_gap; + } + if (key.transform === 'no') { + flags |= constants.key2_key_flags_notransform; + } + const id = sections.strs.allocString(key.id); + const longPress: ListItem = sections.list.allocListFromSpaces(sections.strs, key.longPress); + const longPressDefault = sections.strs.allocString(key.longPressDefault); + const multiTap: ListItem = sections.list.allocListFromSpaces(sections.strs, key.multiTap); + const keySwitch = sections.strs.allocString(key.switch); // 'switch' is a reserved word + flags |= constants.key2_key_flags_extend; + const to = sections.strs.allocString(key.to); // TODO-LDML: single char + const width = Math.ceil((key.width || 1) * 10.0); // default, width=1 + const vkey: any = 0; // TODO-LDML: fill in later + sect.keys.push({ + flags, + flicks, + id, + longPress, + longPressDefault, + multiTap, + switch: keySwitch, // 'switch' is a reserved word + to, + vkey, + width, + }); + } + } +} diff --git a/developer/src/kmc-keyboard/src/compiler/layr.ts b/developer/src/kmc-keyboard/src/compiler/layr.ts new file mode 100644 index 0000000000..b143a86c3c --- /dev/null +++ b/developer/src/kmc-keyboard/src/compiler/layr.ts @@ -0,0 +1,59 @@ +import { constants } from '@keymanapp/ldml-keyboard-constants'; +import { KMXPlus } from '@keymanapp/common-types'; +import { CompilerMessages } from './messages.js'; +import { SectionCompiler } from "./section-compiler.js"; + +import GlobalSections = KMXPlus.GlobalSections; +import Layr = KMXPlus.Layr; +import LayrEntry = KMXPlus.LayrEntry; +import LayrList = KMXPlus.LayrList; +import LayrRow = KMXPlus.LayrRow; +// import USVirtualKeyMap = Constants.USVirtualKeyMap; + +export class LayrCompiler extends SectionCompiler { + + public get id() { + return constants.section.layr; + } + + public validate() { + let valid = true; + if(!this.keyboard.layers?.[0]?.layer?.length) { + valid = false; + this.callbacks.reportMessage(CompilerMessages.Error_MustBeAtLeastOneLayerElement()); + } + // TODO-LDML + return valid; + } + + public compile(sections: GlobalSections): Layr { + const sect = new Layr(); + + sect.lists = this.keyboard.layers.map((layers) => { + const list : LayrList = { + flags: 0, + hardware: sections.strs.allocString(layers.hardware), + minDeviceWidth: layers.minDeviceWidth || 0, + layers: layers.layer.map((layer) => { + const entry : LayrEntry = { + id: sections.strs.allocString(layer.id), + modifier: sections.strs.allocString(layer.modifier), + rows: layer.row.map((row) => { + const erow : LayrRow = { + keys: row.keys.split(' ').map((id) => sections.strs.allocString(id)), + }; + return erow; + }), + }; + // TODO-LDML: modifiers + return entry; + }), + }; + if (layers.form === 'touch') { + list.flags |= constants.layr_list_flags_touch; + } + return list; + }); + return sect; + } +} diff --git a/developer/src/kmc-keyboard/test/fixtures/basic.txt b/developer/src/kmc-keyboard/test/fixtures/basic.txt index 37a6c83dcc..8b848136ec 100644 --- a/developer/src/kmc-keyboard/test/fixtures/basic.txt +++ b/developer/src/kmc-keyboard/test/fixtures/basic.txt @@ -107,9 +107,18 @@ block(sectitems) 66 69 6e 6c diff(sect,finl) + 6b 65 79 32 + diff(sect,key2) + 6b 65 79 73 diff(sect,keys) + 6c 61 79 72 + diff(sect,layr) + + 6c 69 73 74 + diff(sect,list) + 6c 6f 63 61 diff(sect,loca) @@ -255,6 +264,50 @@ block(finl) 00 00 00 00 # KMXPLUS_ELEM before; 01 00 00 00 # KMX_DWORD flags; # }; +# ---------------------------------------------------------------------------------------------------- +# key2 +# ---------------------------------------------------------------------------------------------------- + +block(key2) # struct COMP_KMXPLUS_KEY2 { + 6b 65 79 32 # KMX_DWORD header.ident; // 0000 Section name - key2 + sizeof(key2) # KMX_DWORD header.size; // 0004 Section length + 02 00 00 00 # KMX_DWORD keyCount + 01 00 00 00 # KMX_DWORD flicksCount + 00 00 00 00 # KMX_DWORD flickCount + 00 00 00 00 00 00 00 00 00 00 00 00 # Reserved[3] + # keys + # hmaqtua + 00 00 00 00 # KMX_DWORD vkey + index(strNull,strKey1,2) # KMXPLUS_STR 'U+0127' + 01 00 00 00 # KMX_DWORD (flags: extend) + index(strNull,strHmaqtua,2) # KMXPLUS_STR 'hmaqtua' + 00 00 00 00 # KMXPLUS_STR switch + 0A 00 00 00 # KMX_DWORD width*10 + 01 00 00 00 # TODO: index(listNull,indexAe,4) # LIST longPress 'a e' + 00 00 00 00 # STR longPressDefault + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST multiTap + 00 00 00 00 # flicks 0 + # that + 00 00 00 00 # KMX_DWORD vkey + index(strNull,strKey2,2) # KMXPLUS_STR 'U+0127' + 01 00 00 00 # KMX_DWORD flags = extend + index(strNull,strThat,2) # KMXPLUS_STR 'hmaqtua' + 00 00 00 00 # KMXPLUS_STR switch + 0A 00 00 00 # KMX_DWORD width*10 + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST longPress + 00 00 00 00 # STR longPressDefault + 00 00 00 00 # TODO: index(listNull,listNull,4) # LIST multiTap + 00 00 00 00 # flicks 0 + # flicks + # flicks 0 - null + 00 00 00 00 # KMX_DWORD count + 00 00 00 00 # KMX_DWORD flick + 00 00 00 00 # KMX_STR id + # flick + # Right now there aren't any flick elements. + #00 00 00 00 # LIST directions + #00 00 00 01 # flags + #00 00 00 00 # str: to # ---------------------------------------------------------------------------------------------------- # keys @@ -273,6 +326,71 @@ block(keys) # struct COMP_KMXPLUS_KEYS { c0 00 00 00 00 00 00 00 index(strNull,strKey1,2) 01 00 00 00 # KMX_DWORD vkey, mod, to, flags; 31 00 00 00 00 00 00 00 index(strNull,strKey2,2) 01 00 00 00 # KMX_DWORD vkey, mod, to, flags; +# ---------------------------------------------------------------------------------------------------- +# layr +# ---------------------------------------------------------------------------------------------------- + +block(layr) # struct COMP_KMXPLUS_LAYR { + 6c 61 79 72 # KMX_DWORD header.ident; // 0000 Section name - layr + sizeof(layr) # KMX_DWORD header.size; // 0004 Section length + 01 00 00 00 # KMX_DWORD listCount + 01 00 00 00 # KMX_DWORD layerCount + 01 00 00 00 # KMX_DWORD rowCount + 02 00 00 00 # KMX_DWORD keyCount + 00 00 00 00 # KMX_DWORD reserved0 + 00 00 00 00 # KMX_DWORD reserved1 + # list 0 + 00 00 00 00 # KMX_DWORD flags + index(strNull,strUs,2) # KMXPLUS_STR hardware; + 00 00 00 00 # KMX_DWORD layer; + 01 00 00 00 # count + 7B 00 00 00 # KMX_DWORD minDeviceWidth; // 123 + # layers 0 + index(strNull,strBase,2) # KMXPLUS_STR id; + 00 00 00 00 # KMXPLUS_STR mod str + 00 00 00 00 # KMX_DWORD row index + 01 00 00 00 # KMX_DWORD count + # rows 0 + 00 00 00 00 # KMX_DWORD key index + 02 00 00 00 # KMX_DWORD count + # keys + index(strNull,strHmaqtua,2) # KMXPLUS_STR locale; // 'hmaqtua' + index(strNull,strThat,2) # KMXPLUS_STR locale; // 'that' + +# ---------------------------------------------------------------------------------------------------- +# list +# ---------------------------------------------------------------------------------------------------- + +# TODO-LDML: lots of comment-out ahead. Need to revisit. + +block(list) # struct COMP_KMXPLUS_LAYR_LIST { + 6c 69 73 74 # KMX_DWORD header.ident; // 0000 Section name - list + diff(list,endList) # KMX_DWORD header.size; // 0004 Section length + 02 00 00 00 # KMX_DWORD listCount (should be 2) + 02 00 00 00 # KMX_DWORD indexCount (should be 2) + # list #0 the null list + block(listNull) + 00 00 00 00 #index(indexNull,indexNull,2) # KMX_DWORD list index (0) + 00 00 00 00 # KMX_DWORD lists[0].count + # list #1 the ae list + block(listAe) + 00 00 00 00 # index(indexAe,indexNull,2) # KMX_DWORD list index (also 0) + 02 00 00 00 # KMX_DWORD count + block(endLists) + # indices + #block(indexNull) + # No null index + # index(strNull,strNull,2) # KMXPLUS_STR string index + block(indexAe) + index(strNull,strElemTranFrom2,2) # KMXPLUS_STR a + index(strNull,strElemBkspFrom2,2) # KMXPLUS_STR e + block(endIndices) + block(endList) + +# ---------------------------------------------------------------------------------------------------- +# loca +# ---------------------------------------------------------------------------------------------------- + block(loca) # struct COMP_KMXPLUS_LOCA { 6c 6f 63 61 # KMX_DWORD header.ident; // 0000 Section name - loca sizeof(loca) # KMX_DWORD header.size; // 0004 Section length @@ -280,6 +398,9 @@ block(loca) # struct COMP_KMXPLUS_LOCA { 00 00 00 00 # KMX_DWORD reserved; // 000C padding index(strNull,strLocale,2) # KMXPLUS_STR locale; // 0010+ locale string entry = 'mt' # }; +# ---------------------------------------------------------------------------------------------------- +# meta +# ---------------------------------------------------------------------------------------------------- block(meta) # struct COMP_KMXPLUS_META { 6d 65 74 61 # KMX_DWORD header.ident; // 0000 Section name - meta @@ -292,6 +413,9 @@ block(meta) # struct COMP_KMXPLUS_META { index(strNull,strVersion,2) # KMXPLUS_STR version; 00 00 00 00 # KMX_DWORD settings; # }; +# ---------------------------------------------------------------------------------------------------- +# store_targets_name +# ---------------------------------------------------------------------------------------------------- block(name) # struct COMP_KMXPLUS_META { 6e 61 6d 65 # KMX_DWORD header.ident; // 0000 Section name - name @@ -336,11 +460,15 @@ block(strs) # struct COMP_KMXPLUS_STRS { diff(strs,strName) sizeof(strName,2) diff(strs,strElemTranFrom1) sizeof(strElemTranFrom1,2) diff(strs,strElemTranFrom2) sizeof(strElemTranFrom2,2) + diff(strs,strBase) sizeof(strBase,2) diff(strs,strElemBkspFrom2) sizeof(strElemBkspFrom2,2) + diff(strs,strHmaqtua) sizeof(strHmaqtua,2) diff(strs,strLocale) sizeof(strLocale,2) diff(strs,strLayout) sizeof(strLayout,2) diff(strs,strAuthor) sizeof(strAuthor,2) diff(strs,strConformsTo) sizeof(strConformsTo,2) + diff(strs,strThat) sizeof(strThat,2) + diff(strs,strUs) sizeof(strUs,2) diff(strs,strTranTo) sizeof(strTranTo,2) diff(strs,strKey1) sizeof(strKey1,2) diff(strs,strKey2) sizeof(strKey2,2) @@ -360,21 +488,25 @@ block(strs) # struct COMP_KMXPLUS_STRS { block(strName) 54 00 65 00 73 00 74 00 4b 00 62 00 64 00 block(x) 00 00 # 'TestKbd' block(strElemTranFrom1) 5E 00 block(x) 00 00 # '^' block(strElemTranFrom2) 61 00 block(x) 00 00 # 'a' + block(strBase) 62 00 61 00 73 00 65 00 block(x) 00 00 # 'base' block(strElemBkspFrom2) 65 00 block(x) 00 00 # 'e' + block(strHmaqtua) 68 00 6d 00 61 00 71 00 74 00 75 00 61 00 block(x) 00 00 # 'hmaqtua' block(strLocale) 6d 00 74 00 block(x) 00 00 # 'mt' block(strLayout) 71 00 77 00 65 00 72 00 74 00 79 00 block(x) 00 00 # 'qwerty' block(strAuthor) 73 00 72 00 6c 00 32 00 39 00 35 00 block(x) 00 00 # 'srl295' block(strConformsTo) 74 00 65 00 63 00 68 00 70 00 72 00 65 00 76 00 - 69 00 65 00 77 00 block(x) 00 00 # 'techpreview' + 69 00 65 00 77 00 block(x) 00 00 # 'techpreview' + block(strThat) 74 00 68 00 61 00 74 00 block(x) 00 00 # 'that' + block(strUs) 75 00 73 00 block(x) 00 00 block(strTranTo) E2 00 block(x) 00 00 # 'â' - block(strKey1) 27 01 block(x) 00 00 # 'ħ' - block(strKey2) 90 17 b6 17 block(x) 00 00 # 'ថា' + block(strKey1) 27 01 block(x) 00 00 # 'ħ' + block(strKey2) 90 17 b6 17 block(x) 00 00 # 'ថា' # block(strElemOrdrFrom3) 45 1a block(x) 00 00 # 'ᩅ' block(strElemOrdrFrom1) 60 1a block(x) 00 00 # '᩠' block(strElemOrdrBefore) 6b 1a block(x) 00 00 # 'ᩫ' block(strElemOrdrFrom2) 75 1a block(x) 00 00 # '᩵' - block(strIndicator) 3d d8 40 de block(x) 00 00 # '🙀' + block(strIndicator) 3d d8 40 de block(x) 00 00 # '🙀' diff --git a/developer/src/kmc-keyboard/test/fixtures/basic.xml b/developer/src/kmc-keyboard/test/fixtures/basic.xml index f22a4ff605..fdc8bdfa30 100644 --- a/developer/src/kmc-keyboard/test/fixtures/basic.xml +++ b/developer/src/kmc-keyboard/test/fixtures/basic.xml @@ -25,11 +25,11 @@ - + - + diff --git a/developer/src/kmc-keyboard/test/fixtures/sections/key2/maximal.xml b/developer/src/kmc-keyboard/test/fixtures/sections/key2/maximal.xml new file mode 100644 index 0000000000..10ed505c8b --- /dev/null +++ b/developer/src/kmc-keyboard/test/fixtures/sections/key2/maximal.xml @@ -0,0 +1,37 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/developer/src/kmc-keyboard/test/helpers/index.ts b/developer/src/kmc-keyboard/test/helpers/index.ts index 4453fe1ef1..c3a33755e4 100644 --- a/developer/src/kmc-keyboard/test/helpers/index.ts +++ b/developer/src/kmc-keyboard/test/helpers/index.ts @@ -18,6 +18,7 @@ import Elem = KMXPlus.Elem; import GlobalSections = KMXPlus.GlobalSections; import Section = KMXPlus.Section; import Strs = KMXPlus.Strs; +import List = KMXPlus.List; /** * Builds a path to the fixture with the given path components. @@ -82,9 +83,11 @@ export function loadSectionFixture(compilerClass: typeof SectionCompiler, filena let globalSections: GlobalSections = { strs: new Strs(), - elem: null + elem: null, + list: null, }; globalSections.elem = new Elem(globalSections.strs); + globalSections.list = new List(globalSections.strs); return compiler.compile(globalSections); } diff --git a/developer/src/kmc-keyboard/test/test-key2.ts b/developer/src/kmc-keyboard/test/test-key2.ts new file mode 100644 index 0000000000..4ea12e9075 --- /dev/null +++ b/developer/src/kmc-keyboard/test/test-key2.ts @@ -0,0 +1,65 @@ +import 'mocha'; +import { assert } from 'chai'; +import { Key2Compiler } from '../src/compiler/key2.js'; +import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; +import { KMXPlus } from '@keymanapp/common-types'; +import { constants } from '@keymanapp/ldml-keyboard-constants'; + +import Key2 = KMXPlus.Key2; + +describe('key2', function () { + this.slow(500); // 0.5 sec -- json schema validation takes a while + + it('should compile minimal keys data', function () { + let key2 = loadSectionFixture(Key2Compiler, 'sections/keys/minimal.xml', compilerTestCallbacks) as Key2; + assert.ok(key2); + assert.equal(compilerTestCallbacks.messages.length, 0); + assert.equal(key2.keys.length, 1); + assert.equal(key2.flicks.length, 1); // there's always a 'null' flick + assert.equal(key2.keys[0].to.value, '🪦'); + assert.equal(key2.keys[0].id.value, 'grave'); + }); + + it('should compile maximal key2 data', function () { + let key2 = loadSectionFixture(Key2Compiler, 'sections/key2/maximal.xml', compilerTestCallbacks) as Key2; + assert.ok(key2); + assert.equal(compilerTestCallbacks.messages.length, 0); + assert.equal(key2.keys.length, 4); + + const [q] = key2.keys.filter(({ id }) => id.value === 'q'); + assert.ok(q); + assert.isFalse(!!(q.flags & constants.key2_key_flags_gap)); + assert.equal(q.width, 32); // ceil(3.14159 * 10.0) + assert.equal(q.flicks, 'flick0'); // note this is a string, not a StrsItem + + const [flick0] = key2.flicks.filter(({ id }) => id.value === 'flick0'); + assert.ok(flick0); + assert.equal(flick0.flicks.length, 2); + + const [flick0_nw_se] = flick0.flicks.filter(({ directions }) => directions && directions.isEqual('nw se'.split(' '))); + assert.ok(flick0_nw_se); + assert.equal(flick0_nw_se.to?.value, 'ç'); + + const [flick0_ne_sw] = flick0.flicks.filter(({ directions }) => directions && directions.isEqual('ne sw'.split(' '))); + assert.ok(flick0_ne_sw); + assert.equal(flick0_ne_sw.to?.value, 'ê'); + }); + + it('should accept layouts with gap/switch keys', function () { + let key2 = loadSectionFixture(Key2Compiler, 'sections/keys/gap-switch.xml', compilerTestCallbacks) as Key2; + assert.ok(key2); + assert.equal(compilerTestCallbacks.messages.length, 0); + assert.equal(key2.keys.length, 4); + + const [Qgap] = key2.keys.filter(({ id }) => id.value === 'Q'); + assert.ok(Qgap); + assert.isTrue(!!(Qgap.flags & constants.key2_key_flags_gap)); + + const [Wshift] = key2.keys.filter(({ id }) => id.value === 'W'); + assert.isNotNull(Wshift); + assert.isFalse(!!(Wshift.flags & constants.key2_key_flags_gap)); + assert.equal(Wshift.switch.value, 'shift'); + + }); + +}); diff --git a/developer/src/kmc-keyboard/test/test-layr.ts b/developer/src/kmc-keyboard/test/test-layr.ts new file mode 100644 index 0000000000..2828fdd1b6 --- /dev/null +++ b/developer/src/kmc-keyboard/test/test-layr.ts @@ -0,0 +1,93 @@ +import 'mocha'; +import { assert } from 'chai'; +import { LayrCompiler } from '../src/compiler/layr.js'; +import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js'; +import { KMXPlus } from '@keymanapp/common-types'; +import { constants } from '@keymanapp/ldml-keyboard-constants'; + +import Layr = KMXPlus.Layr; +import LayrRow = KMXPlus.LayrRow; + +function allKeysOk(row : LayrRow, str : string, msg? : string) { + const split = str.split(' '); + assert.equal(row.keys.length, split.length, msg); + for (let i=0; i v.hardware.value === 'abnt2'); + assert.ok(listHardware); + assert.equal(listHardware.minDeviceWidth, 0); + assert.equal(listHardware.layers.length, 2); + assert.equal(listHardware.flags & constants.layr_list_flags_mask_form, constants.layr_list_flags_hardware); + assert.equal(listHardware.hardware?.value, 'abnt2'); + const hardware0 = listHardware.layers[0]; + assert.ok(hardware0); + assert.equal(hardware0.id.value, 'base'); + // assert.equal(hardware0.modifier, ?); // TODO-LDML + const hardware0row0 = hardware0.rows[0]; + assert.ok(hardware0row0); + assert.equal(hardware0row0.keys.length, 2); + allKeysOk(hardware0row0,'Q W', 'hardware0row0'); + const hardware1 = listHardware.layers[1]; + assert.ok(hardware1); + assert.equal(hardware1.rows.length, 1); + assert.equal(hardware1.id.value, 'shift'); + // assert.equal(hardware0.modifier, ?); // TODO-LDML + const hardware1row0 = hardware1.rows[0]; + assert.ok(hardware1row0); + assert.equal(hardware1row0.keys.length, 2); + allKeysOk(hardware1row0,'q w', 'hardware1row0'); + + const listTouch = layr.lists.find(v => v.hardware.value !== 'abnt2'); // TODO-LDML: need to add some more fields!!! + assert.ok(listTouch); + assert.equal(listTouch.minDeviceWidth, 300); + assert.equal(listTouch.layers.length, 1); + assert.equal(listTouch.flags & constants.layr_list_flags_mask_form, constants.layr_list_flags_touch); + const touch0 = listTouch.layers[0]; + assert.ok(touch0); + assert.equal(touch0.rows.length, 1); + assert.equal(touch0.id.value, 'base'); + // assert.equal(touch0.modifier, ?); // TODO-LDML + const touch0row0 = touch0.rows[0]; + assert.ok(touch0row0); + assert.equal(touch0row0.keys.length, 4); + allKeysOk(touch0row0,'Q q W w', 'touch0row0'); + }); +});