Merge pull request #7630 from keymanapp/feat/core/7532-layr-epic-ldml

This commit is contained in:
Steven R. Loomis 2023-01-06 17:40:06 -06:00 committed by GitHub
commit d552a46104
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
25 changed files with 1898 additions and 154 deletions

View file

@ -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 <key> 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 <flicks>, a list of <flick> 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 <flick> 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;
}

View file

@ -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 <layers> 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
};
/**
* <layer> 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
};
/**
* <row> element
*/
interface BUILDER_LAYR_ROW {
key: number; // index into key subtable
count: number; // number of keys
};
/**
* portion of keys attribute of <row>
*/
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 <layers> elements
layers: BUILDER_LAYR_LAYER[], // subtable of <layer> elements
rows: BUILDER_LAYR_ROW[], // subtable of <row> 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;
}

View file

@ -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;
}

View file

@ -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

View file

@ -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[<SectionIdent>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);
}
}
}

View file

@ -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 `<layers>`
*/
export class LayrList {
flags: number;
hardware: StrsItem;
layers: LayrEntry[] = [];
minDeviceWidth: number; // millimeters
};
/**
* In-memory `<layer>`
*/
export class LayrEntry {
id: StrsItem;
modifier: StrsItem;
rows: LayrRow[] = [];
};
/**
* In-memory `<row>`
*/
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'

View file

@ -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<ListIndex> {
/**
* 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<string>) {
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();
}
};

View file

@ -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');

View file

@ -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[];
};

View file

@ -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" */

View file

@ -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--) {

View file

@ -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<const uint8_t *>(this);
rawdata += LDML_LENGTH_LAYR; // skip past non-dynamic portion
// lists
if (layr->listCount > 0) {
lists = reinterpret_cast<const COMP_KMXPLUS_LAYR_LIST *>(rawdata);
} else {
lists = nullptr;
is_valid = false;
}
rawdata += sizeof(COMP_KMXPLUS_LAYR_LIST) * layr->listCount;
// entries
if (layr->layerCount > 0) {
entries = reinterpret_cast<const COMP_KMXPLUS_LAYR_ENTRY *>(rawdata);
} else {
entries = nullptr;
is_valid = false;
}
rawdata += sizeof(COMP_KMXPLUS_LAYR_ENTRY) * layr->layerCount;
// rows
if (layr->rowCount > 0) {
rows = reinterpret_cast<const COMP_KMXPLUS_LAYR_ROW *>(rawdata);
} else {
rows = nullptr;
is_valid = false;
}
rawdata += sizeof(COMP_KMXPLUS_LAYR_ROW) * layr->rowCount;
// keys
if (layr->keyCount > 0) {
keys = reinterpret_cast<const COMP_KMXPLUS_LAYR_KEY *>(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<COMP_KMXPLUS_DISP>(sect);
elem = section_from_sect<COMP_KMXPLUS_ELEM>(sect);
key2 = section_from_sect<COMP_KMXPLUS_KEY2>(sect);
keys = section_from_sect<COMP_KMXPLUS_KEYS>(sect);
layr = section_from_sect<COMP_KMXPLUS_LAYR>(sect);
list = section_from_sect<COMP_KMXPLUS_LIST>(sect);
loca = section_from_sect<COMP_KMXPLUS_LOCA>(sect);
meta = section_from_sect<COMP_KMXPLUS_META>(sect);
strs = section_from_sect<COMP_KMXPLUS_STRS>(sect);
tran = section_from_sect<COMP_KMXPLUS_TRAN>(sect);
vkey = section_from_sect<COMP_KMXPLUS_VKEY>(sect);
// calculate and validate the layer dynamic parts
(void)layrHelper.setLayr(layr);
}
}

View file

@ -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;
};
/**

View file

@ -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 `<layers>` 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 `<layer>` 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

View file

@ -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 didnt, 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
- Its 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 theres 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

View file

@ -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) {

View file

@ -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;
}

View file

@ -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,
});
}
}
}

View file

@ -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;
}
}

View file

@ -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 # 'ថា'
# <reorder before="ᩫ" from="᩠᩵ᩅ" order="10 55 10" />
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 # '🙀'

View file

@ -25,11 +25,11 @@
</displays>
<keys>
<key id="hmaqtua" to="ħ" />
<key id="hmaqtua" to="ħ" longPress="a e" />
<key id="that" to="ថា" />
</keys>
<layers form="hardware">
<layers form="hardware" hardware="us" minDeviceWidth="123">
<layer id="base">
<row keys="hmaqtua that" />
</layer>

View file

@ -0,0 +1,37 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE keyboard SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard.dtd">
<keyboard locale="mt" conformsTo="techpreview">
<names>
<name value="hardware-gap-switch" />
</names>
<keys>
<key id="Q" gap="true" />
<key id="W" switch="shift" />
<!-- This key gets a bunch of other status -->
<key id="q" flicks="flick0" to="q" longPress="á é í" longPressDefault="é"
multiTap="ä ë ï" transform="no" width="3.14159" />
<key id="w" to="w" />
<flicks id="flick0">
<flick directions="nw se" to="ç" />
<flick directions="ne sw" to="ê" />
</flicks>
</keys>
<layers form="touch" minDeviceWidth="300">
<layer id="base">
<row keys="Q q W w" />
</layer>
</layers>
<layers form="hardware" hardware="abnt2">
<layer id="base">
<!-- beware: this is mapping ` and 1! -->
<row keys="Q W" />
</layer>
<layer id="shift">
<!-- beware: this is mapping ` and 1! -->
<row keys="q w" />
</layer>
</layers>
</keyboard>

View file

@ -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);
}

View file

@ -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');
});
});

View file

@ -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<row.keys.length; i++) {
assert.equal(row.keys[i].value, split[i], `${msg||'keys row: '}@#${i}`);
}
}
describe('layr', function () {
this.slow(500); // 0.5 sec -- json schema validation takes a while
// reuse the keys minimal file
it('should compile minimal keys data', function () {
let layr = loadSectionFixture(LayrCompiler, 'sections/keys/minimal.xml', compilerTestCallbacks) as Layr;
assert.ok(layr);
assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(layr.lists?.length, 1);
const list0 = layr.lists[0];
assert.ok(list0);
assert.equal(list0.layers.length, 1);
assert.equal(list0.flags & constants.layr_list_flags_mask_form, constants.layr_list_flags_hardware);
assert.equal(list0.hardware?.value, '');
const layer0 = list0.layers[0];
assert.ok(layer0);
assert.equal(layer0.rows.length, 1);
const row0 = layer0.rows[0];
assert.ok(row0);
assert.equal(row0.keys.length, 1);
assert.equal(layer0.id.value, 'base');
// assert.equal(layr.layers[0].modifier, ?); // TODO-LDML
assert.equal(row0.keys[0]?.value, 'grave');
});
// reuse key2 maximal
it('should compile maximal key2 data', function () {
let layr = loadSectionFixture(LayrCompiler, 'sections/key2/maximal.xml', compilerTestCallbacks) as Layr;
assert.ok(layr);
assert.equal(compilerTestCallbacks.messages.length, 0);
assert.equal(layr.lists?.length, 2);
const listHardware = layr.lists.find(v => 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');
});
});