Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
86 changes: 67 additions & 19 deletions developer/src/kmc-kmn/src/kmw-compiler/validate-layout-file.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,6 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { KMX, TouchLayout } from "@keymanapp/common-types";
import { TouchLayoutFileReader, TouchLayoutFileWriter } from "@keymanapp/developer-utils";
import { callbacks, minimumKeymanVersion, verifyAndSetMinimumRequiredKeymanVersion15,
Expand Down Expand Up @@ -263,6 +266,40 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil
return null;
}

const result = validateLayoutFileContent(data, FDictionary);
if(result === null) {
// A structural error was encountered
return null;
}

// Transform the layout keys with displayMap
if(displayMap) {
Osk.remapTouchLayout(data, displayMap);
}

// If not debugging, then this strips out formatting for a big saving in file size
// This also normalises any values such as Pad or Width which should be strings
const writer = new TouchLayoutFileWriter({formatted: FDebug});

sLayoutFile = writer.compile(data);

sLayoutFile = TransformSpecialKeys14(FDebug, sLayoutFile);

sLayoutFile = TransformSpecialKeys17(FDebug, sLayoutFile);

return {
output: sLayoutFile,
result
}
Comment on lines +275 to +293

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

No changes to this code, just factored the validation out of this function to simplify test.

}

/**
* Validate the structure and content of a touch layout file
* @param data
* @param FDictionary
* @returns null if the file is structurally invalid; false if the file contains content errors, true otherwise
*/
function validateLayoutFileContent(data: TouchLayout.TouchLayoutFile, FDictionary: string[]): boolean | null {
let hasWarnedOfGestureUseDownlevel = false;
const warnGesturesIfNeeded = function(keyId: string) {
if(!hasWarnedOfGestureUseDownlevel && !verifyAndSetMinimumRequiredKeymanVersion17()) {
Expand All @@ -274,6 +311,7 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil
let result: boolean = true;
let FTouchLayoutFont = ''; // I4872
let pid: keyof TouchLayout.TouchLayoutFile;

for(pid in data) {
const platform = data[pid];

Expand All @@ -288,17 +326,33 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil
}

// Test that all required keys are present
if(!Array.isArray(platform.layer)) {
callbacks.reportMessage(KmwCompilerMessages.Error_InvalidTouchLayoutFileFormat({msg: 'platform.layer must be an array'}));
return null;
}
for(const layer of platform.layer) {
const FRequiredKeys: TRequiredKey[] = [];
let rowIndex = 0;
if(!Array.isArray(layer.row)) {
callbacks.reportMessage(KmwCompilerMessages.Error_InvalidTouchLayoutFileFormat({msg: 'layer.row must be an array'}));
return null;
}
for(const row of layer.row) {
rowIndex++;
let keyIndex = 0;
if(!Array.isArray(row.key)) {
callbacks.reportMessage(KmwCompilerMessages.Error_InvalidTouchLayoutFileFormat({msg: 'row.key must be an array'}));
return null;
}
for(const key of row.key) {
keyIndex++;
result = CheckKey(pid, platform, layer, key.id, key.text, key.nextlayer, key.sp, FRequiredKeys, FDictionary, {rowIndex, keyIndex}) && result; // I4119
if(key.sk) {
let subKeyIndex = 0;
if(!Array.isArray(key.sk)) {
callbacks.reportMessage(KmwCompilerMessages.Error_InvalidTouchLayoutFileFormat({msg: 'key.sk must be an array'}));
return null;
}
for(const subkey of key.sk) {
subKeyIndex++;
result = CheckKey(pid, platform, layer, subkey.id, subkey.text, subkey.nextlayer, subkey.sp, FRequiredKeys, FDictionary,
Expand All @@ -307,6 +361,10 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil
}
let direction: keyof TouchLayout.TouchLayoutFlick;
if(key.flick) {
if(typeof(key.flick) != "object") {
callbacks.reportMessage(KmwCompilerMessages.Error_InvalidTouchLayoutFileFormat({msg: 'platform.layer must be an array'}));
return null;
}
for(direction in key.flick) {
warnGesturesIfNeeded(key.id);
result = CheckKey(pid, platform, layer, key.flick[direction].id, key.flick[direction].text,
Expand All @@ -316,6 +374,10 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil

if(key.multitap) {
let multitapIndex = 0;
if(!Array.isArray(key.multitap)) {
callbacks.reportMessage(KmwCompilerMessages.Error_InvalidTouchLayoutFileFormat({msg: 'key.multitap must be an array'}));
return null;
}
for(const subkey of key.multitap) {
multitapIndex++;
warnGesturesIfNeeded(key.id);
Expand All @@ -336,23 +398,9 @@ export function ValidateLayoutFile(fk: KMX.KEYBOARD, FDebug: boolean, sLayoutFil
}
}

// Transform the layout keys with displayMap
if(displayMap) {
Osk.remapTouchLayout(data, displayMap);
}

// If not debugging, then this strips out formatting for a big saving in file size
// This also normalises any values such as Pad or Width which should be strings
const writer = new TouchLayoutFileWriter({formatted: FDebug});

sLayoutFile = writer.compile(data);

sLayoutFile = TransformSpecialKeys14(FDebug, sLayoutFile);

sLayoutFile = TransformSpecialKeys17(FDebug, sLayoutFile);

return {
output: sLayoutFile,
result
}
return result;
}

export const unitTestEndpoints = {
validateLayoutFileContent,
};
52 changes: 52 additions & 0 deletions developer/src/kmc-kmn/test/kmw/validate-layout-file.tests.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by mcdurdin on 2026-08-11
*/
import 'mocha';
import { assert } from 'chai';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
// import { KmnCompiler } from '../../src/compiler/compiler.js';
import { KMX, TouchLayout } from '@keymanapp/common-types';
import { KmwCompilerMessages } from '../../src/kmw-compiler/kmw-compiler-messages.js';
import { unitTestEndpoints } from '../../src/kmw-compiler/validate-layout-file.js';
import { setupGlobals } from '../../src/kmw-compiler/compiler-globals.js';

describe('validate-layout-file', function() {
const callbacks = new TestCompilerCallbacks(this);
const options = {
shouldAddCompilerVersion: false,
saveDebug: true,
};
// const kmnCompiler: KmnCompiler = new KmnCompiler();

this.beforeAll(async function() {
// assert.isTrue(await kmnCompiler.init(callbacks, options));
setupGlobals(callbacks, options, '', '', null, { groups: [], isMnemonic: false, startGroup: { ansi: 0, newContext: 0, postKeystroke: 0, unicode: 0 }, stores: [], targets: '', fileVersion: KMX.KMXFile.VERSION_100}, '');
});

this.afterEach(function() {
if (this.currentTest.state !== 'passed') {
callbacks.printMessages();
}
})

const fixtures: {name: string, data: TouchLayout.TouchLayoutFile}[] = [
{ name: 'a null platform.layer property', data: { desktop: { layer: null, defaultHint: null } } },
{ name: 'an invalid platform.layer property', data: { desktop: { layer: {} as any, defaultHint: null } } },
{ name: 'a null platform.layer.row property', data: { desktop: { layer: [{id:'', row: null}], defaultHint: null } } },
{ name: 'an invalid platform.layer.row property', data: { desktop: { layer: [{id:'', row: "" as any}], defaultHint: null } } },
{ name: 'a null platform.layer.row.key property', data: { desktop: { layer: [{id:'', row: [{id:'', key: null}]}], defaultHint: null } } },
{ name: 'an invalid platform.layer.row.key property', data: { desktop: { layer: [{id:'', row: [{id:'', key: 100}]}], defaultHint: null } } },
{ name: 'an invalid platform.layer.row.key.sk property', data: { desktop: { layer: [{id:'', row: [{id:'', key: [{sk: "x" as any}]}]}], defaultHint: null } } },
{ name: 'an invalid platform.layer.row.key.multitap property', data: { desktop: { layer: [{id:'', row: [{id:'', key: [{multitap: "x" as any}]}]}], defaultHint: null } } },
{ name: 'an invalid platform.layer.row.key.flick property', data: { desktop: { layer: [{id:'', row: [{id:'', key: [{flick: "x" as any}]}]}], defaultHint: null } } },
]

fixtures.forEach(fixture => {
it(`should raise an error if a file has ${fixture.name}`, function() {
assert.isNull(unitTestEndpoints.validateLayoutFileContent(fixture.data, []));
assert.isTrue(callbacks.hasMessage(KmwCompilerMessages.ERROR_InvalidTouchLayoutFileFormat));
});
});
});