Skip to content

Commit 99663ce

Browse files
authored
Merge pull request #9621 from keymanapp/chore/developer/9620-validate-keyboard_info-and-fix-keys
chore(developer): validate emitted .keyboard_info and fix keys 🎺
2 parents 1fa540c + 893c492 commit 99663ce

File tree

16 files changed

+172
-389
lines changed

16 files changed

+172
-389
lines changed

common/schemas/keyboard_info/keyboard_info.schema.json

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@
2222
"jsFilename": { "type": "string", "pattern": "\\.js$" },
2323
"jsFileSize": { "type": "number" },
2424
"isRTL": { "type": "boolean" },
25-
"encodings": { "type": "array", "items": { "type": "string", "enum": ["ansi", "unicode"] }, "additionalItems": false },
26-
"packageIncludes": { "type": "array", "items": { "type": "string", "enum": ["welcome", "documentation", "fonts", "visualKeyboard"] }, "additionalItems": false },
25+
"encodings": { "type": "array", "items": { "type": "string", "enum": ["ansi", "unicode"] } },
26+
"packageIncludes": { "type": "array", "items": { "type": "string", "enum": ["welcome", "documentation", "fonts", "visualKeyboard"] } },
2727
"version": { "type": "string" },
2828
"minKeymanVersion": { "type": "string", "pattern": "^\\d+\\.\\d$" },
2929
"helpLink": { "type": "string", "pattern": "^https://help\\.keyman\\.com/keyboard/" },

common/web/types/src/package/kmp-json-file.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -97,7 +97,17 @@ export interface KmpJsonFileExample {
9797
*/
9898
id: string;
9999
/**
100-
* A space-separated list of keys, modifiers indicated with "+", spacebar is "space", plus key is "shift+=" or "plus"
100+
* A space-separated list of keys.
101+
* - modifiers indicated with "+"
102+
* - spacebar is "space"
103+
* - plus key is "shift+=" or "plus" on US English (all other punctuation as per key cap).
104+
* - Hardware modifiers are: "shift", "ctrl", "alt", "left-ctrl",
105+
* "right-ctrl", "left-alt", "right-alt"
106+
* - Key caps should generally be their character for desktop (Latin script
107+
* case insensitive), or the actual key cap for touch
108+
* - Caps Lock should be indicated with "caps-on", "caps-off"
109+
*
110+
* e.g. "shift+a b right-alt+c space plus z z z" represents something like: "Ab{AltGr+C} +zzz"
101111
*/
102112
keys: string;
103113
/**

common/web/types/src/package/kps-file.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -184,7 +184,17 @@ export interface KpsFileLanguageExample {
184184
*/
185185
ID: string;
186186
/**
187-
* A space-separated list of keys, modifiers indicated with "+", spacebar is "space", plus key is "shift+=" or "plus"
187+
* A space-separated list of keys.
188+
* - modifiers indicated with "+"
189+
* - spacebar is "space"
190+
* - plus key is "shift+=" or "plus" on US English (all other punctuation as per key cap).
191+
* - Hardware modifiers are: "shift", "ctrl", "alt", "left-ctrl",
192+
* "right-ctrl", "left-alt", "right-alt"
193+
* - Key caps should generally be their character for desktop (Latin script
194+
* case insensitive), or the actual key cap for touch
195+
* - Caps Lock should be indicated with "caps-on", "caps-off"
196+
*
197+
* e.g. "shift+a b right-alt+c space plus z z z" represents something like: "Ab{AltGr+C} +zzz"
188198
*/
189199
Keys: string;
190200
/**

developer/src/common/delphi/compiler/Keyman.Developer.System.ValidateKpsFile.pas

Lines changed: 0 additions & 70 deletions
This file was deleted.

developer/src/kmc-keyboard-info/package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
"dependencies": {
2727
"@keymanapp/common-types": "*",
2828
"@keymanapp/kmc-package": "*",
29-
"@keymanapp/developer-utils": "*"
29+
"@keymanapp/developer-utils": "*",
30+
"ajv": "^8.11.0",
31+
"ajv-formats": "^2.1.1"
3032
},
3133
"bundleDependencies": [
3234
"@keymanapp/developer-utils"
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
import { KeyboardInfoFileExampleKey } from "./keyboard-info-file.js";
2+
3+
/**
4+
* Converts a .kps or .kmp example keys string into an array of key objects
5+
* matching the .keyboard_info example file format
6+
* @param keysString
7+
* @returns
8+
*/
9+
export function packageKeysExamplesToKeyboardInfo(keysString: string): KeyboardInfoFileExampleKey[] {
10+
const items = keysString.trim().split(/ +/);
11+
const result: KeyboardInfoFileExampleKey[] = [];
12+
for(const item of items) {
13+
const keyAndModifiers = item.split('+');
14+
if(keyAndModifiers.length > 0) {
15+
const key: KeyboardInfoFileExampleKey = {key: keyAndModifiers.pop()}
16+
if(keyAndModifiers.length) {
17+
key.modifiers = [...keyAndModifiers];
18+
};
19+
result.push(key);
20+
}
21+
}
22+
return result;
23+
}

developer/src/kmc-keyboard-info/src/index.ts

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,14 @@ import langtags from "./imports/langtags.js";
1111
import { validateMITLicense } from "@keymanapp/developer-utils";
1212
import { KmpCompiler } from "@keymanapp/kmc-package";
1313

14+
import AjvModule from 'ajv';
15+
import AjvFormatsModule from 'ajv-formats';
16+
const Ajv = AjvModule.default; // The actual expected Ajv type.
17+
const ajvFormats = AjvFormatsModule.default;
18+
19+
import { Schemas } from "@keymanapp/common-types";
20+
import { packageKeysExamplesToKeyboardInfo } from "./example-keys.js";
21+
1422
const regionNames = new Intl.DisplayNames(['en'], { type: "region" });
1523
const scriptNames = new Intl.DisplayNames(['en'], { type: "script" });
1624
const langtagsByTag = {};
@@ -281,6 +289,20 @@ export class KeyboardInfoCompiler {
281289
}
282290

283291
const jsonOutput = JSON.stringify(keyboard_info, null, 2);
292+
293+
// TODO: look at performance improvements by precompiling Ajv schemas on first use
294+
const ajv = new Ajv({ logger: {
295+
log: (message) => this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Hint_OutputValidation({message})),
296+
warn: (message) => this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Warn_OutputValidation({message})),
297+
error: (message) => this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Error_OutputValidation({message})),
298+
}});
299+
ajvFormats.default(ajv);
300+
if(!ajv.validate(Schemas.default.keyboard_info, keyboard_info)) {
301+
// This is an internal fatal error; we should not be capable of producing
302+
// invalid output, so it is best to throw and die
303+
throw new Error(ajv.errorsText());
304+
}
305+
284306
return new TextEncoder().encode(jsonOutput);
285307
}
286308

@@ -380,9 +402,9 @@ export class KeyboardInfoCompiler {
380402
if(example.id == bcp47) {
381403
language.examples.push({
382404
// we don't copy over example.id
383-
keys:example.keys,
384-
note:example.note,
385-
text:example.text
405+
keys: packageKeysExamplesToKeyboardInfo(example.keys),
406+
note: example.note,
407+
text: example.text
386408
});
387409
}
388410
}

developer/src/kmc-keyboard-info/src/keyboard-info-file.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,12 @@ export interface KeyboardInfoFileLanguageFont {
4949
}
5050

5151
export interface KeyboardInfoFileExample {
52-
keys?: string;
52+
keys?: KeyboardInfoFileExampleKey[];
5353
text?: string;
5454
note?: string;
5555
}
56+
57+
export interface KeyboardInfoFileExampleKey {
58+
key: string;
59+
modifiers?: string[];
60+
}

developer/src/kmc-keyboard-info/src/messages.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m
22

33
const Namespace = CompilerErrorNamespace.KeyboardInfoCompiler;
44
// const SevInfo = CompilerErrorSeverity.Info | Namespace;
5-
// const SevHint = CompilerErrorSeverity.Hint | Namespace;
5+
const SevHint = CompilerErrorSeverity.Hint | Namespace;
66
const SevWarn = CompilerErrorSeverity.Warn | Namespace;
77
const SevError = CompilerErrorSeverity.Error | Namespace;
88
const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
@@ -45,5 +45,17 @@ export class KeyboardInfoCompilerMessages {
4545
static Error_NoLicenseFound = () => m(this.ERROR_NoLicenseFound,
4646
`No license for the keyboard was found. MIT license is required for publication to Keyman keyboards repository.`);
4747
static ERROR_NoLicenseFound = SevError | 0x000A;
48+
49+
static Hint_OutputValidation = (o:{message: any}) => m(this.HINT_OutputValidation,
50+
`Validating output: ${o.message}.`);
51+
static HINT_OutputValidation = SevHint | 0x000B;
52+
53+
static Warn_OutputValidation = (o:{message: any}) => m(this.WARN_OutputValidation,
54+
`Validating output: ${o.message}.`);
55+
static WARN_OutputValidation = SevWarn | 0x000C;
56+
57+
static Error_OutputValidation = (o:{message: any}) => m(this.ERROR_OutputValidation,
58+
`Validating output: ${o.message}.`);
59+
static ERROR_OutputValidation = SevError | 0x000D;
4860
}
4961

developer/src/kmc-keyboard-info/test/fixtures/khmer_angkor/build/khmer_angkor.keyboard_info

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,13 @@
1515
]
1616
},
1717
"examples": [{
18-
"keys": "x j m E r",
18+
"keys": [
19+
{ "key": "x" },
20+
{ "key": "j" },
21+
{ "key": "m" },
22+
{ "key": "E" },
23+
{ "key": "r" }
24+
],
1925
"text": "\u1781\u17D2\u1798\u17C2\u179A",
2026
"note": "Name of language"
2127
}],

0 commit comments

Comments
 (0)