chore(windows): Merge branch 'master' into feat/windows/kmshell-switch-handling

# Keyman Conventional Commit suggestions:
#
# - Link to a Sentry issue with git trailer:
#     Fixes: _MODULE_-_ID_
# - Give credit to co-authors:
#     Co-authored-by: _Name_ <_email_>
# - Use imperative, present tense ('attach' not 'attaches', 'attached' etc)
# - Don't include a period at the end of the title
# - Always include a blank line before trailers
# - More: https://github.com/keymanapp/keyman/wiki/Pull-Request-and-Commit-workflow-notes
This commit is contained in:
rc-swag 2025-01-22 15:47:37 +10:00
commit 521c78c5dd
56 changed files with 1288 additions and 106 deletions

View file

@ -1,5 +1,36 @@
# Keyman Version History
## 18.0.174 alpha 2025-01-21
* chore(developer): validate targets parameter in kmc-generate (#12955)
* fix(developer): support `&displayMap` font in web debugger (#12929)
## 18.0.173 alpha 2025-01-20
* feat(developer): rewrite font data in .kvk from package metadata (#12949)
* docs(ios): document gestures within iOS app help (#12900)
* fix(developer): include developer-utils deps in server build (#12951)
## 18.0.172 alpha 2025-01-19
* chore(common): Add 17.0.333 to version history (#12926)
## 18.0.171 alpha 2025-01-18
* fix(common/web): add StrsItem.isEqual() method (#12868)
* fix(common/web): handle invalid order and tertiary arguments to ElementString.fromString() (#12882)
## 18.0.170 alpha 2025-01-17
* chore(linux): update copyright year (#12918)
* fix(web): patch unit test with execution dependent on autocorrect state (#12940)
* test(common/web): unit tests for element-string (#12811)
* chore(linux): Update debian changelog (#12917)
## 18.0.169 alpha 2025-01-17
* chore(windows): remove `postinstall` state from mermaid diagram (#12923)
## 18.0.168 alpha 2025-01-16
* chore: update macOS environment-variable shell script (#12878)
@ -949,7 +980,7 @@
## 18.0.41 alpha 2024-05-22
* fix(developer): handle `KM_CORE_IT_INVALIDATE_CONTEXT` in debugger (#11488)
* chore(linux): Trigger GHA packaging for stable builds :cherries: (#11495)
* chore(linux): Trigger GHA packaging for stable builds (#11495)
* chore(android,mac,windows): Update crowdin strings for DE (#11497)
* feat(web): custom infrastructure for @web/test-runner use (#11403)
* chore(web): conversion of lm-worker browser-test for @web/test-runner use (#11404)
@ -1145,6 +1176,17 @@
* chore(common): move to 18.0 alpha (#10713)
* chore: move to 18.0 alpha
## 17.0.333 stable 2025-01-16
* fix(core): permanently disable logging (#12674)
* fix(linux): pushing of updated changelog branch (#12819)
* fix(linux): work around Lintian errors (#12817)
* fix(core): implement ldml_processor::get_key_list() (#12816)
* chore(linux): Update debian changelog (#12022)
* fix(android): use main looper to dispatch key events when OSK is hidden (#12875)
* chore: use GitHub PR titles when writing HISTORY.md (#12908)
* fix(developer): filter incorrect fonts out of .keyboard_info (#12913)
## 17.0.332 stable 2024-11-06
* fix(developer): create Server config directory before options save (#12609)

View file

@ -1 +1 @@
18.0.169
18.0.175

View file

@ -19,8 +19,8 @@ export class ElemElement {
order: number; // -128 to +127; used only by reorder element values
tertiary: number; // -128 to +127; used only by reorder element values
flags: ElemElementFlags;
isEqual(a: ElemElement) {
return a.value === this.value &&
isEqual(a: ElemElement): boolean {
return a.value.isEqual(this.value) &&
a.order === this.order &&
a.tertiary === this.tertiary &&
a.flags === this.flags;
@ -104,8 +104,8 @@ export class ElementString extends Array<ElemElement> {
typeFlag |= constants.elem_flags_type_str;
}
}
elem.order = orders.length ? parseInt(orders[i], 10) : 0;
elem.tertiary = tertiaries.length ? parseInt(tertiaries[i], 10) : 0;
elem.order = orders.length ? this.parseIntOrZero(orders[i]) : 0;
elem.tertiary = tertiaries.length ? this.parseIntOrZero(tertiaries[i]) : 0;
elem.flags = ElemElementFlags.none |
(ElemElementFlags.type & typeFlag) |
(tertiary_bases?.[i] == '1' /* TODO-LDML: or 'true'? */ ? ElemElementFlags.tertiary_base : 0) |
@ -125,5 +125,9 @@ export class ElementString extends Array<ElemElement> {
}
return true;
}
private static parseIntOrZero(str: string): number {
const num = parseInt(str, 10);
return !Number.isNaN(num) ? num : 0;
}
}
;

View file

@ -131,6 +131,10 @@ export class StrsItem {
get isOneChar() {
return this.char !== undefined;
}
isEqual(a: StrsItem): boolean {
return a.value === this.value && a.char === this.char;
}
};
/**

View file

@ -0,0 +1,498 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by Dr Mark C. Sinclair on 2024-12-09
*
* Test code for element-string.ts
*/
import 'mocha';
import { assert } from 'chai';
import { constants } from '@keymanapp/ldml-keyboard-constants';
import { ElemElementFlags, ElemElement, ElementString } from '../../../src/kmx/kmx-plus/element-string.js';
import { StrsItem, UsetItem, Strs, StrsOptions, DependencySections, CharStrsItem, Uset } from '../../../src/kmx/kmx-plus/kmx-plus.js';
import { UnicodeSet, UnicodeSetParser } from '../../../src/ldml-keyboard/unicodeset-parser-api.js';
import { ElementParser, ElementSegment, ElementType } from '../../../src/ldml-keyboard/pattern-parser.js';
const GOTHIC_A = new StrsItem("𐌰", 0x10330);
const GOTHIC_B = new StrsItem("𐌱", 0x10331);
const GOTHIC_C = new StrsItem("𐌲", 0x10332);
const GOTHIC_D = new StrsItem("𐌳", 0x10333);
const HI_GOTHIC_A = new StrsItem('\ud800', 0xd800);
const LO_GOTHIC_A = new StrsItem('\udf30', 0xdf30);
const GOTHIC_PATTERN = "[𐌰-𐍊]";
const GOTHIC_STRSITEM = new StrsItem(GOTHIC_PATTERN);
const GOTHIC_SET = new UnicodeSet(GOTHIC_PATTERN, [[0x10330,0x1034A]]);
const GOTHIC_USETITEM = new UsetItem(GOTHIC_SET, GOTHIC_STRSITEM);
const UGARITIC_PATTERN = "[𐎀-𐎟]";
const UGARITIC_STRSITEM = new StrsItem(UGARITIC_PATTERN);
const UGARITIC_SET = new UnicodeSet(UGARITIC_PATTERN, [[0x10380,0x1039F]]);
const UGARITIC_USETITEM = new UsetItem(UGARITIC_SET, UGARITIC_STRSITEM);
class TestUnicodeSetParser implements UnicodeSetParser {
parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => GOTHIC_SET;
sizeUnicodeSet = (pattern: string) : number => 1;
};
const origElementParserSegment = ElementParser.segment;
let sections: DependencySections = null;
describe('Test of ElementString file', () => {
describe('Test of ElemElement', () => {
describe('Test of isEqual()', () => {
it('returns true when elems identical', () => {
const one = initElemElement();
const two = initElemElement();
assert.isTrue(one.isEqual(two));
});
it('returns true when elems are clones', () => {
const one = initElemElement(new StrsItem("𐌰", 0x10330));
const two = initElemElement(new StrsItem("𐌰", 0x10330));
assert.isTrue(one.isEqual(two));
});
it('returns false when value differs', () => {
const one = initElemElement(GOTHIC_A);
const two = initElemElement(GOTHIC_B);
assert.isFalse(one.isEqual(two));
});
it('returns false when order differs', () => {
const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0);
const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 1);
assert.isFalse(one.isEqual(two));
});
it('returns false when tertiary differs', () => {
const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0);
const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 1);
assert.isFalse(one.isEqual(two));
});
it('returns false when flags differs', () => {
const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0, ElemElementFlags.none);
const two = initElemElement(GOTHIC_A, GOTHIC_USETITEM, 0, 0, ElemElementFlags.type);
assert.isFalse(one.isEqual(two));
});
it('returns true even though uset differs', () => {
const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM);
const two = initElemElement(GOTHIC_A, UGARITIC_USETITEM);
assert.isTrue(one.isEqual(two));
});
});
});
describe('Test of ElementString class', () => {
beforeEach(() => {
sections = {
strs: new Strs(),
uset: new Uset(),
usetparser: new TestUnicodeSetParser(),
};
ElementParser.segment = stubElementParserSegment_CodePoint;
});
afterEach(() => {
ElementParser.segment = origElementParserSegment;
});
describe('Test of fromStrings()', () => {
it('returns an empty ElementString if source is null', () => {
const es = ElementString.fromStrings({}, null);
assert.deepEqual(es, new ElementString());
});
it('can create an ElementString from a string array', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]);
const expected = [
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
];
assert.deepEqual(actual, expected);
});
it('can create an ElementString from a string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲");
const expected = [
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
];
assert.deepEqual(actual, expected);
});
it('can apply order string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
"1 2 3",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 1),
initElemElement(GOTHIC_B, undefined, 2),
initElemElement(GOTHIC_C, undefined, 3),
];
assert.deepEqual(actual, expected);
});
it('can apply single order to all', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
"1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 1),
initElemElement(GOTHIC_B, undefined, 1),
initElemElement(GOTHIC_C, undefined, 1),
];
assert.deepEqual(actual, expected);
});
it('can handle order string that is too short', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
"1 2",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 1),
initElemElement(GOTHIC_B, undefined, 2),
initElemElement(GOTHIC_C, undefined, 0),
];
assert.deepEqual(actual, expected);
});
it('can handle non-number in order string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
"1 A 3",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 1),
initElemElement(GOTHIC_B, undefined, 0),
initElemElement(GOTHIC_C, undefined, 3),
];
assert.deepEqual(actual, expected);
});
it('can apply tertiary string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
"1 2 3",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 1),
initElemElement(GOTHIC_B, undefined, 0, 2),
initElemElement(GOTHIC_C, undefined, 0, 3),
];
assert.deepEqual(actual, expected);
});
it('can apply single tertiary to all', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
"1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 1),
initElemElement(GOTHIC_B, undefined, 0, 1),
initElemElement(GOTHIC_C, undefined, 0, 1),
];
assert.deepEqual(actual, expected);
});
it('can handle tertiary string that is too short', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
"1 2",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 1),
initElemElement(GOTHIC_B, undefined, 0, 2),
initElemElement(GOTHIC_C, undefined, 0, 0),
];
assert.deepEqual(actual, expected);
});
it('can handle non-number in tertiary string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
"1 A 3",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 1),
initElemElement(GOTHIC_B, undefined, 0, 0),
initElemElement(GOTHIC_C, undefined, 0, 3),
];
assert.deepEqual(actual, expected);
});
it('can apply tertiary_base string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
"1 0 1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.tertiary_base),
];
assert.deepEqual(actual, expected);
});
it('can apply single tertiary_base to all', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
"1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.tertiary_base),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.tertiary_base),
];
assert.deepEqual(actual, expected);
});
it('can handle tertiary_base string that is too short', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
"1 0",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.tertiary_base),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.none),
];
assert.deepEqual(actual, expected);
});
it('can apply prebase string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
null,
"1 0 1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.prebase),
];
assert.deepEqual(actual, expected);
});
it('can apply single prebase to all', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
null,
"1",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.prebase),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.prebase),
];
assert.deepEqual(actual, expected);
});
it('can handle prebase string that is too short', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
"𐌰𐌱𐌲",
null,
null,
null,
"1 0",
);
const expected = [
initElemElement(GOTHIC_A, undefined, 0, 0, ElemElementFlags.prebase),
initElemElement(GOTHIC_B, undefined, 0, 0, ElemElementFlags.none),
initElemElement(GOTHIC_C, undefined, 0, 0, ElemElementFlags.none),
];
assert.deepEqual(actual, expected);
});
it('can create an ElementString from a uset string', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.strs.allocString = stubStrsAllocString_Str;
sections.uset.allocUset = stubUsetAllocUset;
const actual = ElementString.fromStrings(sections, "[𐌰-𐍊]");
const expected = [
initElemElement(
new StrsItem(''),
GOTHIC_USETITEM,
0,
0,
constants.elem_flags_type_uset,
),
];
assert.deepEqual(actual, expected);
});
it('returns null for an invalid unicode set size', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.usetparser.sizeUnicodeSet = (pattern: string) : number => -1;
assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]"));
});
it('returns null if it cannot parse the unicode set', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.usetparser.parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => null;
assert.isNull(ElementString.fromStrings(sections, "[𐌰-𐍊]"));
});
it('can handle quad strings', () => {
sections.strs.allocString = stubStrsAllocString_Char;
ElementParser.segment = stubElementParserSegment_Escaped;
const actual = ElementString.fromStrings(sections, "\\ud800\\udf30");
const expected = [
initElemElement(HI_GOTHIC_A),
initElemElement(LO_GOTHIC_A),
];
assert.deepEqual(actual, expected);
});
it('can handle ElemElement of string type', () => {
sections.strs.allocString = stubStrsAllocString_Str;
const actual = ElementString.fromStrings(sections, ["𐌰𐌱𐌲",]);
const expected = [
initElemElement(
new StrsItem("𐌰𐌱𐌲"),
undefined,
0,
0,
constants.elem_flags_type_str,
),
];
assert.deepEqual(actual, expected);
});
});
describe('Test of isEqual()', () => {
it('returns true when ElementStrings are identical', () => {
const es = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
assert.isTrue(es.isEqual(es));
});
it('returns true when ElementStrings are clones', () => {
const one = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
const two = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
assert.isTrue(one.isEqual(two));
});
it('returns false when ElementStrings are different lengths', () => {
const one = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
const two = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
]);
assert.isFalse(one.isEqual(two));
});
it('returns false when ElementStrings have different ElemElements', () => {
const one = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_C),
]);
const two = initElementString([
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
initElemElement(GOTHIC_D),
]);
assert.isFalse(one.isEqual(two));
});
});
describe('Test of parseIntOrZero()', () => {
it('returns a number for a valid string', () => {
const num = ElementString['parseIntOrZero']('1');
assert.equal(num, 1);
});
it('returns zero for an invalid string', () => {
const num = ElementString['parseIntOrZero']('A');
assert.equal(num, 0);
});
it('returns zero for undefined', () => {
const num = ElementString['parseIntOrZero'](undefined);
assert.equal(num, 0);
});
it('returns zero for a null string', () => {
const num = ElementString['parseIntOrZero'](null);
assert.equal(num, 0);
});
});
});
});
function initElemElement(
value: StrsItem = GOTHIC_A,
uset: UsetItem = undefined,
order: number = 0,
tertiary: number = 0,
flags: ElemElementFlags = ElemElementFlags.none,
): ElemElement {
const ee = new ElemElement();
ee.value = value;
ee.uset = uset;
ee.order = order;
ee.tertiary = tertiary;
ee.flags = flags;
return ee;
};
function initElementString(elemElements: ElemElement[]): ElementString {
const es: ElementString = new ElementString();
elemElements.forEach((ee) => {es.push(ee)});
return es;
};
function stubStrsAllocString_Char(s?: string, opts?: StrsOptions, sections?: DependencySections): StrsItem {
return new CharStrsItem(s);
};
function stubStrsAllocString_Str(s?: string, opts?: StrsOptions, sections?: DependencySections): StrsItem {
return new StrsItem(s);
};
function stubElementParserSegment_CodePoint(str: string): ElementSegment[] {
return [...str].map(s => new ElementSegment(s, ElementType.codepoint));
};
function stubElementParserSegment_Uset(str: string): ElementSegment[] {
return [new ElementSegment(str, ElementType.uset)];
};
function stubElementParserSegment_Escaped(str: string): ElementSegment[] {
const strs = str.match(/\\u[0-9a-fA-F]{4}/g);
return strs.map((s) => new ElementSegment(s, ElementType.escaped));
};
function stubUsetAllocUset(set: UnicodeSet, sections: DependencySections) : UsetItem {
return new UsetItem(set, new StrsItem(set.pattern));
};

View file

@ -230,7 +230,8 @@ type
ssPlatform = 31, ssBaseLayout = 32, ssLayer = 33, ssVKDictionary = 34, ssLayoutFile = 35, // I3438 // I3483
ssKeyboardVersion = 36, ssKMW_EmbedCSS = 37, ssTargets = 38,
ssCasedKeys = 39,
ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41, ssNewLayer = 42, ssOldLayer = 43); // I4140 // I4368 // I4504
ssBegin_NewContext = 40, ssBegin_PostKeystroke = 41, ssNewLayer = 42, ssOldLayer = 43, // I4140 // I4368 // I4504
ssDisplayMap = 44);
const
SystemStoreNames: array[TSystemStore] of WideString = (
@ -245,7 +246,8 @@ const
'', '', '', '', 'LAYOUTFILE', 'KEYBOARDVERSION', 'KMW_EMBEDCSS',
'TARGETS', //9.0 // I3483 // I4140 // I4368 // I4504
'CASEDKEYS', //14.0,
'', '', 'NEWLAYER', 'OLDLAYER'); //15.0
'', '', 'NEWLAYER', 'OLDLAYER', //15.0
'DISPLAYMAP'); //17.0
implementation

View file

@ -21,7 +21,8 @@ unit KeyboardFonts;
interface
type
TKeyboardFont = (kfontCode, kfontChar, kfontOSK, kfontTouchLayoutPhone, kfontTouchLayoutTablet, kfontTouchLayoutDesktop);
TKeyboardFont = (kfontCode, kfontChar, kfontOSK, kfontTouchLayoutPhone, kfontTouchLayoutTablet, kfontTouchLayoutDesktop,
kfontDisplayMap);
TKeyboardFontArray = array[TKeyboardFont] of string; // I4409
@ -34,7 +35,7 @@ type
const
KeyboardFontId: array[TKeyboardFont] of string = (
'Code', 'Char', 'Osk', 'TouchLayoutPhone', 'TouchLayoutTablet', 'TouchLayoutDesktop'
'Code', 'Char', 'Osk', 'TouchLayoutPhone', 'TouchLayoutTablet', 'TouchLayoutDesktop', 'DisplayMap'
);
implementation

View file

@ -24,5 +24,32 @@ export async function getFontFamily(source: Uint8Array) {
return null;
}
return font.meta.property.find(prop => prop.name == 'font-family')?.text ?? null;
}
/**
* Extracts the font-family from an in-memory TTF or WOFF blob in `source`
* parameter.
*
* @param source In-memory TTF or WOFF font blob
*
* @throws Uncaught exceptions from ttfMeta.promise if the font file is invalid.
*
* @returns If the file is invalid or cannot be parsed, returns `null`,
* otherwise returns the font family as a string.
*/
export function getFontFamilySync(source: Uint8Array) {
/* c8 ignore next 3 */
if(!source) {
return null;
}
const buffer = Buffer.from(source);
const font = ttfMeta.ttfInfoSync(buffer);
/* c8 ignore next 3 */
if(!font) {
return null;
}
return font.meta.property.find(prop => prop.name == 'font-family')?.text ?? null;
}

View file

@ -68,4 +68,8 @@ export * as SourceFilenamePatterns from './source-filename-patterns.js';
export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js';
export * as GitHubUrls from './github-urls.js';
export * as CloudUrls from './cloud-urls.js';
export * as CloudUrls from './cloud-urls.js';
export { getFontFamily, getFontFamilySync } from './font-family.js';
export * as ValidIds from './valid-ids.js';

View file

@ -31,6 +31,20 @@ export function ttfInfo(pathOrData, callback) {
}
}
/**
* @namespace
* @param {string | number | Buffer | URL} pathOrData
* @returns {typeof ttf.result}
*/
export function ttfInfoSync(pathOrData) {
if (pathOrData instanceof Buffer) {
return ttf.ttfInfoSync(ttf.view(pathOrData));
} else {
const data = fs.readFileSync(pathOrData);
return ttf.ttfInfoSync(ttf.view(data));
}
}
/**
* @param {string | number | Buffer | URL} pathOrData
* @returns {Promise<typeof ttf.result>}
@ -48,5 +62,5 @@ export function promise(pathOrData) {
}
/** @namespace */
export const ttfMeta = { ttfInfo, promise };
export const ttfMeta = { ttfInfo, ttfInfoSync, promise };
export default ttfMeta;

View file

@ -300,6 +300,16 @@ export function ttfInfo(data, callback) {
}
}
/**
* @namespace
* @param {*} data
* @returns {typeof result}
*/
export function ttfInfoSync(data) {
resultTables(data);
return result;
}
/**
* @param {string | number | Buffer | URL | DataView} pathOrData
* @returns {Promise<typeof result>}

View file

@ -0,0 +1,38 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
/**
* Verifies that an id passed in is valid as an identifier for a Keyman .kmn
* keyboard: starts with alpha or underscore and remainder is alphanumeric
* @param id
* @returns
*/
export function isValidKeymanKeyboardId(id: string) {
return /^[a-z_][a-z0-9_]*$/.test(id);
}
/**
* Verifies that an id passed in is valid as an identifier for an LDML xml
* keyboard: starts with alpha or underscore and remainder is alphanumeric.
* Note that this test is not the same as the CLDR test for valid LDML xml
* filename, as this test has a narrower validity policy to match the Keyman
* implementation requirements.
* @param id
* @returns
*/
export function isValidLdmlKeyboardId(id: string) {
return /^[a-z_][a-z0-9_]*$/.test(id);
}
/**
* Verifies that an id passed in is valid as an identifier for a Keyman
* lexical model: starts with alpha or underscore and remainder is alphanumeric,
* in three components -- author.bcp47.uniq
* @param id
* @returns
*/
export function isValidLexicalModelId(id: string) {
return /^[a-z_][a-z0-9_]*\.[a-z][a-z0-9_]+\.[a-z_][a-z0-9_]*$/.test(id);
}

View file

@ -0,0 +1,76 @@
/*
* Keyman is copyright (C) SIL Global. MIT License.
*/
import { assert } from 'chai';
import 'mocha';
import { isValidKeymanKeyboardId, isValidLdmlKeyboardId, isValidLexicalModelId } from '../src/valid-ids.js';
describe('valid-ids', function () {
it('should accept a valid Keyman keyboard ID', function() {
[
'khmer_angkor',
'm',
'fancypants1_2',
'keyboard___',
'_100',
].forEach(id => assert.isTrue(isValidKeymanKeyboardId(id), `expected '${id}' to be valid`));
});
it('should reject invalid Keyman keyboard IDs', function() {
[
'khmer_angkor@example.com',
'keyboard ',
'Khmer',
' keyboard',
'111',
'?',
'keyboard.version',
].forEach(id => assert.isFalse(isValidKeymanKeyboardId(id), `expected '${id}' to be invalid`));
});
it('should accept a valid LDML keyboard ID', function() {
[
'khmer_angkor',
'm',
'fancypants1_2',
'keyboard___',
'_100',
].forEach(id => assert.isTrue(isValidLdmlKeyboardId(id), `expected '${id}' to be valid`));
});
it('should reject invalid LDML keyboard IDs', function() {
[
'khmer_angkor@example.com',
'keyboard ',
' keyboard',
'Khmer',
'111',
'?',
'keyboard.version',
].forEach(id => assert.isFalse(isValidLdmlKeyboardId(id), `expected '${id}' to be invalid`));
});
it('should accept a valid lexical model ID', function() {
[
'khmer.km.khmer_angkor',
'nrc.en.mtnt',
].forEach(id => assert.isTrue(isValidLexicalModelId(id), `expected '${id}' to be valid`));
});
it('should reject invalid lexical model IDs', function() {
[
'khmer',
'en-us',
'khmer.km-KM.angkor',
'Khmer.km_km.angkor',
'model ',
' model',
'111',
'?',
'lexical.model',
'lexical..model',
'.en.model',
].forEach(id => assert.isFalse(isValidLexicalModelId(id), `expected '${id}' to be invalid`));
});
});

View file

@ -5,8 +5,12 @@
"rootDir": ".",
"outDir": "./build/",
"baseUrl": ".",
/** for ttfmeta */
"allowJs": true,
},
"include": [
"src/**/*.ts",
"src/ttfmeta/lib/*.js",
],
}

View file

@ -54,7 +54,8 @@
"exclude-after-remap": true,
"exclude": [
"test/",
"src/template/"
"src/template/",
"src/main.ts"
]
},
"repository": {

View file

@ -7,6 +7,7 @@
import { KeymanTargets } from "@keymanapp/common-types";
import KEYMAN_VERSION from "@keymanapp/keyman-version";
import { AbstractGenerator, GeneratorArtifacts } from "./abstract-generator.js";
import { GeneratorMessages } from "./generator-messages.js";
/**
* @internal
@ -28,7 +29,19 @@ export class BasicGenerator extends AbstractGenerator {
this.languageTags = this.options.languageTags.length
? this.options.languageTags.map(tag => new Intl.Locale(tag).minimize().toString())
: [this.DEFAULT_LOCALE];
//TODO-GENERATE: validate targets
// Verify that targets passed in are valid
let failed = false;
for(const target of this.options.targets) {
if(!KeymanTargets.AllKeymanTargets.includes(target)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidTarget({target}));
failed = true;
}
}
if(failed) {
return false;
}
if(this.options.targets.includes(KeymanTargets.KeymanTarget.any) || this.options.targets.length == 0) {
this.resolvedTargets = KeymanTargets.AllKeymanTargets.filter(target => target != KeymanTargets.KeymanTarget.any);
} else {
@ -50,6 +63,8 @@ export class BasicGenerator extends AbstractGenerator {
dt.getDate().toString().padStart(2, '0');
this.tokenMap['$PACKAGE_LANGUAGES'] = this.generateLanguageListForPackage();
this.tokenMap['$PLATFORMS_DOTLIST_README'] = this.getPlatformDotListForReadme();
return true;
}
protected generate(artifacts: GeneratorArtifacts): boolean {
@ -89,6 +104,8 @@ export class BasicGenerator extends AbstractGenerator {
sourceFile = this.callbacks.path.join(this.templateBasePath, this.templatePath, sourceFile);
const sourceData = this.callbacks.loadFile(sourceFile);
/* c8 ignore next 4 */
if(sourceData == null) {
// internal error -- source file should be readable
throw new Error(`source file ${sourceFile} does not exist`);

View file

@ -17,30 +17,65 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
* @internal
*/
export class GeneratorMessages {
static Fatal_UnexpectedException = (o:{e: any}) => m(this.FATAL_UnexpectedException, null, o.e ?? 'unknown error');
static FATAL_UnexpectedException = SevFatal | 0x0001;
static Fatal_UnexpectedException = (o:{e: any}) => m(
this.FATAL_UnexpectedException, null, o.e ?? 'unknown error'
);
static Info_GeneratingProject = (o:{type: string, id: string}) => m(this.INFO_GeneratingProject,
`Generating project of type ${def(o.type)} with id ${def(o.id)}`);
static INFO_GeneratingProject = SevInfo | 0x0002;
static Info_GeneratingProject = (o:{type: string, id: string}) => m(
this.INFO_GeneratingProject,
`Generating project of type ${def(o.type)} with id ${def(o.id)}`
);
static ERROR_CannotCreateFolder = SevError | 0x0003;
static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotCreateFolder, null,
`Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}`);
static Error_CannotCreateFolder = (o:{folderName:string, e: any}) => CompilerMessageSpecWithException(
this.ERROR_CannotCreateFolder, null,
`Unable to create folder ${def(o.folderName)}: ${o.e ?? 'unknown error'}`
);
static ERROR_OutputPathAlreadyExists = SevError | 0x0004;
static Error_OutputPathAlreadyExists = (o:{outPath:string}) => m(
this.ERROR_OutputPathAlreadyExists,
`Output path ${def(o.outPath)} already exists, not overwriting`);
`Output path ${def(o.outPath)} already exists, not overwriting`
);
static ERROR_CannotWriteOutputFile = SevError | 0x0005;
static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException(this.ERROR_CannotWriteOutputFile, null,
`Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}`);
static Error_CannotWriteOutputFile = (o:{filename:string, e: any}) => CompilerMessageSpecWithException(
this.ERROR_CannotWriteOutputFile, null,
`Unable to write file ${def(o.filename)}: ${o.e ?? 'unknown error'}`
);
// See also PackageCompilerMessages.WARN_PackageNameDoesNotFollowLexicalModelConventions
static WARN_ModelIdDoesNotFollowLexicalModelConventions = SevWarn | 0x0006;
static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m(this.WARN_ModelIdDoesNotFollowLexicalModelConventions,
static Warn_ModelIdDoesNotFollowLexicalModelConventions = (o:{id: string}) => m(
this.WARN_ModelIdDoesNotFollowLexicalModelConventions,
`The id ${def(o.id)} does not follow the recommended model id conventions. The id should be all lower case, `+
`include only alphanumeric characters and underscore (_), not start with a digit, and should have the structure `+
`<author>.<bcp47>.<uniq>`);
`<author>.<bcp47>.<uniq>`
);
static ERROR_InvalidTarget = SevError | 0x0007;
static Error_InvalidTarget = (o:{target:string}) => m(
this.ERROR_InvalidTarget,
`The specified target platform '${def(o.target)}' is not a valid target.`
);
static ERROR_InvalidKeymanKeyboardId = SevError | 0x0008;
static Error_InvalidKeymanKeyboardId = (o:{id:string}) => m(
this.ERROR_InvalidKeymanKeyboardId,
`The specified keyboard id '${def(o.id)}' contains characters that are not permitted for a .kmn keyboard id or filename.`
);
static ERROR_InvalidLdmlKeyboardId = SevError | 0x0009;
static Error_InvalidLdmlKeyboardId = (o:{id:string}) => m(
this.ERROR_InvalidLdmlKeyboardId,
`The specified keyboard id '${def(o.id)}' contains characters that are not permitted for an LDML .xml keyboard id or filename.`
);
static ERROR_InvalidLexicalModelId = SevError | 0x000A;
static Error_InvalidLexicalModelId = (o:{id:string}) => m(
this.ERROR_InvalidLexicalModelId,
`The specified lexical model id '${def(o.id)}' contains characters that are not permitted or does not match the required pattern of 'author.bcp47.uniq'.`
);
};

View file

@ -5,9 +5,10 @@
*/
import { KeymanFileTypes, KeymanTargets } from '@keymanapp/common-types';
import { KeymanCompiler, } from '@keymanapp/developer-utils';
import { KeymanCompiler, ValidIds, } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
import { GeneratorMessages } from './generator-messages.js';
/**
* @public
@ -39,7 +40,15 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise<GeneratorResult> {
this.preGenerate();
if(!ValidIds.isValidKeymanKeyboardId(this.options.id)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidKeymanKeyboardId({id:this.options.id}));
return null;
}
if(!this.preGenerate()) {
// errors will have been reported in preGenerate
return null;
}
const artifacts: GeneratorArtifacts = this.defaultArtifacts();

View file

@ -5,9 +5,10 @@
*/
import { KeymanFileTypes } from '@keymanapp/common-types';
import { KeymanCompiler, } from '@keymanapp/developer-utils';
import { KeymanCompiler, ValidIds, } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
import { GeneratorMessages } from './generator-messages.js';
/**
* @public
@ -28,7 +29,15 @@ export class LdmlKeyboardGenerator extends BasicGenerator implements KeymanCompi
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise<GeneratorResult> {
this.preGenerate();
if(!ValidIds.isValidLdmlKeyboardId(this.options.id)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidLdmlKeyboardId({id:this.options.id}));
return null;
}
if(!this.preGenerate()) {
// errors will have been reported in preGenerate
return null;
}
const artifacts: GeneratorArtifacts = this.defaultArtifacts();
this.templatePath = 'ldml-keyboard';

View file

@ -5,7 +5,7 @@
*/
import { KeymanFileTypes } from '@keymanapp/common-types';
import { KeymanCompiler, SourceFilenamePatterns } from '@keymanapp/developer-utils';
import { KeymanCompiler, SourceFilenamePatterns, ValidIds } from '@keymanapp/developer-utils';
import { GeneratorArtifacts, GeneratorResult } from './abstract-generator.js';
import { BasicGenerator } from './basic-generator.js';
import { GeneratorMessages } from './generator-messages.js';
@ -31,7 +31,15 @@ export class LexicalModelGenerator extends BasicGenerator implements KeymanCompi
* @returns Binary artifacts on success, null on failure.
*/
async run(): Promise<GeneratorResult> {
this.preGenerate();
if(!ValidIds.isValidLexicalModelId(this.options.id)) {
this.callbacks.reportMessage(GeneratorMessages.Error_InvalidLexicalModelId({id:this.options.id}));
return null;
}
if(!this.preGenerate()) {
// errors will have been reported in preGenerate
return null;
}
const artifacts: GeneratorArtifacts = this.defaultArtifacts();

View file

@ -2,22 +2,71 @@
* Keyman is copyright (C) SIL International. MIT License.
*/
import * as fs from 'node:fs';
import * as path from 'node:path';
import 'mocha';
import { assert } from 'chai';
import { BasicGenerator } from '../src/basic-generator.js';
import { TestCompilerCallbacks } from '@keymanapp/developer-test-helpers';
import { BasicGenerator } from '../src/basic-generator.js';
import { AbstractGenerator, GeneratorArtifacts } from '../src/abstract-generator.js';
import { options } from './shared-options.js';
import { makePathToFixture } from './helpers/index.js';
// this class is here to make it possible to access protected members
class BasicGeneratorTest extends BasicGenerator {
static files() {
return [
'sample/' + AbstractGenerator.SFile_WelcomeHTM,
'sample/' + AbstractGenerator.SFile_ReadmeHTM,
'sample/' + AbstractGenerator.SFile_HistoryMD,
'sample/' + AbstractGenerator.SFile_LicenseMD,
'sample/' + AbstractGenerator.SFile_ReadmeMD,
'sample/' + AbstractGenerator.SFile_GitIgnore,
];
};
setup() {
this.templatePath = 'kmn-keyboard';
}
}
describe('BasicGenerator', function () {
it('should configure default settings', async function() {
const bg = new BasicGenerator();
const callbacks = new TestCompilerCallbacks();
assert(await bg.init(callbacks, options));
bg.test_preGenerate();
assert.isTrue(await bg.init(callbacks, options));
assert.isTrue(bg.test_preGenerate());
assert.equal(bg.test_tokenMap['$NAME'], 'Sample Project');
});
// TODO-GENERATE: test transform
it('should successfully transform common project files', async function() {
const bg = new BasicGeneratorTest();
bg.setup();
const callbacks = new TestCompilerCallbacks();
assert.isTrue(await bg.init(callbacks, options));
assert.isTrue(bg.test_preGenerate());
const artifacts: GeneratorArtifacts = { "kmc-generate:outputPath": { data: null, filename: '' } };
assert.isTrue(bg.test_generate(artifacts));
// standard artifacts are listed in AbstractGenerator
const keys = [].concat(
'kmc-generate:outputPath',
BasicGeneratorTest.files().map(f => f.replaceAll('/', path.sep))
);
assert.hasAllKeys(artifacts, keys);
// pick one file and compare it
const filename = BasicGeneratorTest.files()[0].replaceAll('/', path.sep); // welcome.htm
assert.equal(artifacts[filename].filename, filename);
const fixturePath = makePathToFixture('basic', 'welcome.htm');
const fixture = fs.readFileSync(fixturePath, 'utf-8').replace(/\r\n/g, '\n');
const actual = new TextDecoder().decode(artifacts[filename].data);
assert.equal(actual, fixture);
});
});

View file

@ -0,0 +1,26 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Start Using Sample Project</title>
<style type="text/css">
p { font: 10pt Tahoma; }
h1 { font: bold 16pt Tahoma; color: #4444cc; margin-bottom: 2px }
h2 { font: bold 12pt Tahoma; color: #4444cc; }
</style>
</head>
<body>
<h1>Start Using Sample Project</h1>
<p>
# A mighty description
</p>
<h1>Keyboard Layout</h1>
<!-- Insert Keyboard Layout Images or HTML here -->
</body>
</html>

View file

@ -2,15 +2,24 @@
* Keyman is copyright (C) SIL International. MIT License.
*/
import * as path from 'node:path';
import * as fs from 'node:fs';
import 'mocha';
import * as path from 'path';
import * as fs from 'fs';
import { fileURLToPath } from 'url';
import { assert } from 'chai';
import { GeneratorMessages } from '../src/generator-messages.js';
import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers';
import { CompilerErrorNamespace } from '@keymanapp/developer-utils';
import { GeneratorMessages } from '../src/generator-messages.js';
import { AbstractGenerator, GeneratorOptions } from '../src/abstract-generator.js';
import { BasicGenerator } from '../src/basic-generator.js';
import { KeymanKeyboardGenerator } from '../src/keyman-keyboard-generator.js';
import { LdmlKeyboardGenerator } from '../src/ldml-keyboard-generator.js';
import { LexicalModelGenerator } from '../src/lexical-model-generator.js';
import { options } from './shared-options.js';
describe('GeneratorMessages', function () {
const callbacks = new TestCompilerCallbacks();
@ -62,4 +71,49 @@ describe('GeneratorMessages', function () {
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_CannotWriteOutputFile),
`messageId ERROR_CannotWriteOutputFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
});
it('should generate ERROR_InvalidTarget if an invalid target is passed in', async function () {
const testOptions: GeneratorOptions = { ...options,
// @ts-ignore ('invalid' could be passed by a user, even if would be a compile error here)
targets: ['invalid']
};
const bg = new BasicGenerator();
const callbacks = new TestCompilerCallbacks();
assert(await bg.init(callbacks, testOptions));
assert.isFalse(bg.test_preGenerate());
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidTarget));
});
it('should generate ERROR_InvalidKeymanKeyboardId if an invalid keyboard id is passed in', async function () {
const testOptions: GeneratorOptions = { ...options,
id: '???invalid',
};
const kkg = new KeymanKeyboardGenerator();
const callbacks = new TestCompilerCallbacks();
assert.isTrue(await kkg.init(callbacks, testOptions));
assert.isNull(await kkg.run());
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidKeymanKeyboardId));
});
it('should generate ERROR_InvalidLdmlKeyboardId if an invalid keyboard id is passed in', async function () {
const testOptions: GeneratorOptions = { ...options,
id: '???invalid',
};
const lkg = new LdmlKeyboardGenerator();
const callbacks = new TestCompilerCallbacks();
assert.isTrue(await lkg.init(callbacks, testOptions));
assert.isNull(await lkg.run());
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLdmlKeyboardId));
});
it('should generate ERROR_InvalidLexicalModelId if an invalid lexical model id is passed in', async function () {
const testOptions: GeneratorOptions = { ...options,
id: 'example.???.invalid',
};
const lmg = new LexicalModelGenerator();
const callbacks = new TestCompilerCallbacks();
assert.isTrue(await lmg.init(callbacks, testOptions));
assert.isNull(await lmg.run());
assert.isTrue(callbacks.hasMessage(GeneratorMessages.ERROR_InvalidLexicalModelId));
});
});

View file

@ -12,7 +12,7 @@ import { CompilerCallbacks, KeymanCompiler, CompilerOptions, KeymanCompilerResul
import { KmpCompiler } from "@keymanapp/kmc-package";
import { SchemaValidators } from "@keymanapp/common-types";
import { getFontFamily } from "./font-family.js";
import { getFontFamily } from "@keymanapp/developer-utils";
const regionNames = new Intl.DisplayNames(['en'], { type: "region" });
const scriptNames = new Intl.DisplayNames(['en'], { type: "script" });

View file

@ -2,7 +2,7 @@ import * as fs from 'fs';
import { assert } from 'chai';
import 'mocha';
import { makePathToFixture } from './helpers/index.js';
import { getFontFamily } from '../src/font-family.js';
import { getFontFamily } from '@keymanapp/developer-utils';
const AFGHAN_TURKMEN_DISPLAY_FONT = makePathToFixture('afghan_turkmen', "Lateef-Regular.ttf");
const AFGHAN_TURKMEN_OSK_FONT = makePathToFixture('afghan_turkmen', "Lateef-Bold.ttf");

View file

@ -13,7 +13,6 @@
},
"include": [
"src/**/*.ts",
"src/ttfmeta/lib/*.js",
"src/imports/langtags.js",
],
"references": [

View file

@ -18,6 +18,8 @@ import { PackageKeyboardTargetValidator } from './package-keyboard-target-valida
import { PackageMetadataUpdater } from './package-metadata-updater.js';
import { markdownToHTML } from './markdown.js';
import { PackageValidation } from './package-validation.js';
import { getFontFamilySync } from "@keymanapp/developer-utils";
import { setKvkFontData } from './kvk-font-facename.js';
const KMP_JSON_FILENAME = 'kmp.json';
const KMP_INF_FILENAME = 'kmp.inf';
@ -483,36 +485,36 @@ export class KmpCompiler implements KeymanCompiler {
this.callbacks.reportMessage(PackageCompilerMessages.Warn_AbsolutePath({filename: filename}));
}
let memberFileData = this.getMemberFileData(kpsFilename, filename);
if(!memberFileData) {
failed = true;
return;
}
filename = this.callbacks.resolveFilename(kpsFilename, filename);
const basename = this.callbacks.path.basename(filename);
if(!this.callbacks.fs.existsSync(filename)) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename}));
failed = true;
return;
if(KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Binary.VisualKeyboard)) {
if(!this.isKvkFileBinary(memberFileData)) {
// warn the few users who are still doing this -- non-binary .kvk should not be included in package!
this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename}));
} else {
memberFileData = this.setKvkFontData(kpsFilename, data, filename, memberFileData);
}
}
let memberFileData;
try {
memberFileData = this.callbacks.loadFile(filename);
} catch(e) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e}));
failed = true;
return;
}
this.warnIfKvkFileIsNotBinary(filename, memberFileData);
zip.file(basename, memberFileData);
// Remove path data from files before JSON save
value.name = basename;
});
if(failed) {
return null;
}
data.files.forEach((value) => {
const basename = this.callbacks.path.basename(value.name);
// Remove path data from files before JSON save
value.name = basename;
});
// TODO #9477: transform .md to .htm
// Remove path data from file references in options
@ -557,26 +559,87 @@ export class KmpCompiler implements KeymanCompiler {
return transcodeToCP1252(s);
}
private getMemberFileData(kpsFilename: string, filename: string) {
filename = this.callbacks.resolveFilename(kpsFilename, filename);
if(!this.callbacks.fs.existsSync(filename)) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileDoesNotExist({filename: filename}));
return null;
}
try {
return this.callbacks.loadFile(filename);
} catch(e) {
this.callbacks.reportMessage(PackageCompilerMessages.Error_FileCouldNotBeRead({filename: filename, e: e}));
return null;
}
}
private setKvkFontData(kpsFilename: string, data: KmpJsonFile.KmpJsonFile, filename: string, kvk: Uint8Array) {
// find the appropriate font to set
const kvkId = this.callbacks.path.basename(filename.toLowerCase(), '.kvk');
const kbd = data.keyboards.find(k => k.id == kvkId);
if(!kbd) {
// cannot find a matching keyboard
this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindMatchingKeyboardForVisualKeyboard({filename}));
return kvk;
}
const fontFilename = kbd.oskFont || kbd.displayFont;
if(!fontFilename) {
// no font data to set
return kvk;
}
// Look up the full font filename
const fontFile = data.files.find(file => this.callbacks.path.basename(file.name.toLowerCase()) == fontFilename.toLowerCase());
if(!fontFile) {
// font cannot be found
this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindFontForKeyboard({id: kbd.id, fontFilename}));
return kvk;
}
// the font is a filename, included in the .kps
const fontData = this.getMemberFileData(kpsFilename, fontFile.name);
if(!fontData) {
// cannot find source font
this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotFindFontForKeyboard({id: kbd.id, fontFilename: fontFile.name}));
return kvk;
}
const fontFacename = getFontFamilySync(fontData);
if(!fontFacename) {
// Font facename could not be extracted from ttf
this.callbacks.reportMessage(PackageCompilerMessages.Warn_CannotReadFont({fontFilename: fontFile.name}));
return kvk;
}
const result = setKvkFontData(kvk, fontFacename);
if(!result) {
// KVK is invalid
this.callbacks.reportMessage(PackageCompilerMessages.Warn_VisualKeyboardFileIsInvalid({filename}));
return kvk;
}
return result;
}
/**
* Legacy .kmp compiler would transform xml-format .kvk files into a binary .kvk file; now
* we want that to remain the responsibility of the keyboard compiler, so we'll warn the
* few users who are still doing this
* we want that to remain the responsibility of the keyboard compiler.
*/
private warnIfKvkFileIsNotBinary(filename: string, data: Uint8Array) {
if(!KeymanFileTypes.filenameIs(filename, KeymanFileTypes.Binary.VisualKeyboard)) {
return;
}
private isKvkFileBinary(data: Uint8Array) {
if(data.byteLength < 4) {
// TODO: Not a valid .kvk file; should we be reporting this?
return;
return false;
}
// Must start with 'KVKF'
if(data[0] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[0] ||
data[1] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[1] ||
data[2] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[2] ||
data[3] != KvkFile.KVK_HEADER_IDENTIFIER_BYTES[3]) {
this.callbacks.reportMessage(PackageCompilerMessages.Warn_FileIsNotABinaryKvkFile({filename: filename}));
return false;
}
return true;
}
}

View file

@ -0,0 +1,16 @@
import { KvkFileReader, KvkFileWriter } from "@keymanapp/common-types";
export function setKvkFontData(fileData: Uint8Array, fontFacename: string) {
const reader = new KvkFileReader();
const kvk = reader.read(fileData);
if(!kvk) {
// kvk is invalid
return null;
}
kvk.header.ansiFont.name = fontFacename;
kvk.header.unicodeFont.name = fontFacename;
const writer = new KvkFileWriter();
return writer.write(kvk);
}

View file

@ -164,5 +164,25 @@ export class PackageCompilerMessages {
static Error_RequiredParameterMissing = (o:{param: string}) => m(
this.ERROR_RequiredParameterMissing, `Source parameter '${def(o.param)}' is required.`
);
static WARN_CannotFindMatchingKeyboardForVisualKeyboard = SevWarn | 0x0023;
static Warn_CannotFindMatchingKeyboardForVisualKeyboard = (o:{filename: string}) => m(
this.WARN_CannotFindMatchingKeyboardForVisualKeyboard, `Cannot find matching keyboard for visual keyboard file '${def(o.filename)}'.`
);
static WARN_CannotFindFontForKeyboard = SevWarn | 0x0024;
static Warn_CannotFindFontForKeyboard = (o:{id: string, fontFilename: string}) => m(
this.WARN_CannotFindFontForKeyboard, `Cannot find font file '${def(o.fontFilename)}' for keyboard '${def(o.id)}'.`
);
static WARN_CannotReadFont = SevWarn | 0x0025;
static Warn_CannotReadFont = (o:{fontFilename: string}) => m(
this.WARN_CannotReadFont, `Cannot parse font file '${def(o.fontFilename)}' to get facename.`
);
static WARN_VisualKeyboardFileIsInvalid = SevWarn | 0x0026;
static Warn_VisualKeyboardFileIsInvalid = (o:{filename: string}) => m(
this.WARN_VisualKeyboardFileIsInvalid, `Visual keyboard file '${def(o.filename)}' is invalid.`
);
}

View file

@ -27,7 +27,7 @@
<FileType>.kvk</FileType>
</File>
<File>
<Name>../../invalid/basic.kmx</Name>
<Name>binary_kvk_file.kmx</Name>
<Description>Keyboard Basic</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
@ -36,7 +36,7 @@
<Keyboards>
<Keyboard>
<Name>Basic</Name>
<ID>basic</ID>
<ID>binary_kvk_file</ID>
<Version>1.0</Version>
<Languages>
<Language ID="KM">Khmer</Language>

View file

@ -45,7 +45,7 @@ function declareGenerateLdmlKeyboard(command: Command) {
.option('-o, --out-path <path>', 'Output path (may exist)')
.option('-n, --name <name>', 'Keyboard descriptive name')
.option('-c, --copyright <copyright-name>', 'Copyright holder') /* © yyyy <copyright-name> */
.option('-v, --version <version-string>', 'Keyboard version', '1.0')
.option('-v, --version <version-string>', 'Keyboard version', '1.0.0')
.option('-L, --language-tag <bcp-47 tag>', 'BCP-47 language tag',
(value, previous) => previous.concat([value]), [])
.option('-a, --author <author-name>', 'Name of keyboard author')
@ -59,12 +59,12 @@ function declareGenerateLexicalModel(command: Command) {
modelSubCommand
.description('Generate a wordlist lexical model project')
.option('-o, --out-path <path>', 'Output path (may exist)')
.option('-n, --name <name>', 'Keyboard descriptive name')
.option('-n, --name <name>', 'Lexical model descriptive name')
.option('-c, --copyright <copyright-name>', 'Copyright holder') /* © yyyy <copyright-name> */
.option('-v, --version <version-string>', 'Keyboard version', '1.0')
.option('-v, --version <version-string>', 'Lexical model version', '1.0')
.option('-L, --language-tag <bcp-47 tag>', 'BCP-47 language tag',
(value, previous) => previous.concat([value]), [])
.option('-a, --author <author-name>', 'Name of keyboard author')
.option('-a, --author <author-name>', 'Name of lexical model author')
.option('-d, --description <description>', 'Short description of the project, Markdown')
.action(generateLexicalModel);

View file

@ -113,7 +113,17 @@ function installer_server() {
# monorepo context
cat package.json | "$JQ" \
'. | del(.devDependencies."@keymanapp/resources-gosh") | del(.devDependencies."@keymanapp/keyman-version") | del(.dependencies."@keymanapp/developer-utils")' \
> "$PRODBUILDTEMP/package0.json"
# #12950: Merge dependencies from @keymanapp/developer-utils and Server; this
# is a bit messy but will mean we don't lose the needed dependencies.
"$JQ" -s '.[0] + { dependencies: (.[1].dependencies * .[0].dependencies) }' "$PRODBUILDTEMP/package0.json" \
"$KEYMAN_ROOT/developer/src/common/web/utils/package.json" > "$PRODBUILDTEMP/package1.json"
cat $PRODBUILDTEMP/package1.json | "$JQ" \
'. | del(.dependencies."@keymanapp/common-types")' \
> "$PRODBUILDTEMP/package.json"
rm "$PRODBUILDTEMP/package0.json"
rm "$PRODBUILDTEMP/package1.json"
pushd "$PRODBUILDTEMP"
npm install --omit=dev --omit=optional

View file

@ -369,6 +369,8 @@ type
FLayoutSetup: Boolean;
FCheckboxGridHelper: TCheckboxGridHelper;
FDisplayMapFont: TFont;
function GetCurrentRule: TKeyboardParser_LayoutRule;
procedure EnableControls;
@ -475,6 +477,7 @@ type
procedure OrderDetailsPanels;
function ValidateFileHasNoUnicodeCharacters(sw: WideString): Boolean;
procedure UpdateQRCode;
procedure SetDisplayMapFont(const Value: TFont);
protected
function GetHelpTopic: string; override;
@ -495,6 +498,8 @@ type
function ShouldRememberFocus(Control: TWinControl): Boolean; override; // I4679
property DisplayMapFont: TFont read FDisplayMapFont write SetDisplayMapFont;
public
procedure StartDebugging(FStartTest: Boolean = False);
procedure StopDebugging;
@ -630,6 +635,8 @@ procedure TfrmKeymanWizard.FormCreate(Sender: TObject);
begin
FLoading := True;
FDisplayMapFont := TFont.Create;
//SetWindowTheme(pages.Handle, 'Explorer', '');
cmdAddToProject.Caption := '&Add to Project';
@ -681,6 +688,7 @@ begin
FreeAndNil(FKeyboardParser); // I4557
FreeAndNil(FDebugForm);
FreeAndNil(FCheckboxGridHelper);
FreeAndNil(FDisplayMapFont);
UninitSystemKeyboard;
end;
@ -1825,6 +1833,15 @@ begin
Delete(Result.Size, Length(Result.Size)-1, 2);
end;
end;
kfontDisplayMap:
begin
Result.Enabled := FKeyboardParser.GetSystemStoreValue(ssDisplayMap) <> '';
if Result.Enabled then
begin
Result.Name := DisplayMapFont.Name;
Result.Size := IntToStr(TFontUtils.FontSizeInPoints(DisplayMapFont.Name, DisplayMapFont.Size)); // I4872
end;
end;
end;
end;
@ -2535,6 +2552,8 @@ begin
FFont.Free;
end;
end;
SetFontFromString(DisplayMapFont, ProjectFile.IDEState['DisplayMapFont']);
end;
procedure TfrmKeymanWizard.SaveSettings(SaveProject: Boolean);
@ -2548,6 +2567,7 @@ begin
else ProjectFile.IDEState['DebugDefaultFont'] := '0'; // I4702
ProjectFile.IDEState['DebugFont'] := FontAsString(FDebugForm.memo.Font); // I4702
ProjectFile.IDEState['DisplayMapFont'] := FontAsString(DisplayMapFont);
inherited;
end;
@ -2781,6 +2801,13 @@ begin
TUtilExecute.URL(lbDebugHosts.Items[lbDebugHosts.ItemIndex]);
end;
procedure TfrmKeymanWizard.SetDisplayMapFont(const Value: TFont);
begin
FDisplayMapFont.Assign(Value);
if ProjectFile <> nil then
ProjectFile.IDEState['DisplayMapFont'] := FontAsString(DisplayMapFont);
end;
procedure TfrmKeymanWizard.SetFontInfo(Index: TKeyboardFont; const Value: TKeyboardFontInfo); // I4057
var
f: TFont;
@ -2814,6 +2841,11 @@ begin
CharFont.Name := Value.Name;
CharFont.Size := StrToIntDef(Value.Size, 12);
end;
kfontDisplayMap:
begin
DisplayMapFont.Name := Value.Name;
DisplayMapFont.Size := StrToIntDef(Value.Size, 12);
end;
kfontOSK:
begin
if FKeyboardParser.Features.ContainsKey(kfOSK) then // I4258

View file

@ -2,11 +2,11 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
BorderIcons = [biSystemMenu]
BorderStyle = bsDialog
Caption = 'Keyboard Fonts'
ClientHeight = 291
ClientHeight = 322
ClientWidth = 512
Position = poScreenCenter
ExplicitWidth = 518
ExplicitHeight = 320
ExplicitHeight = 351
PixelsPerInch = 96
TextHeight = 13
object lblDevEnv: TLabel
@ -147,6 +147,20 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
Height = 13
Caption = 'em'
end
object lblDisplayMap: TLabel
Left = 32
Top = 248
Width = 109
Height = 13
Caption = '&displayMap target font'
end
object Label4: TLabel
Left = 463
Top = 248
Width = 14
Height = 13
Caption = 'em'
end
object fcbCode: TscFontComboBox
Left = 176
Top = 41
@ -260,23 +274,23 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
end
object cmdOK: TButton
Left = 162
Top = 248
Top = 280
Width = 91
Height = 25
Caption = 'OK'
Default = True
TabOrder = 12
TabOrder = 14
OnClick = cmdOKClick
end
object cmdCancel: TButton
Left = 259
Top = 248
Top = 280
Width = 90
Height = 25
Cancel = True
Caption = 'Cancel'
ModalResult = 2
TabOrder = 13
TabOrder = 15
end
object fcbTouchLayoutPhone: TscFontComboBox
Left = 176
@ -347,4 +361,34 @@ inherited frmKeyboardFonts: TfrmKeyboardFonts
ShowPreviewFontName = False
ShowPreviewInList = False
end
object editDisplayMapSize: TEdit
Left = 391
Top = 245
Width = 66
Height = 21
TabOrder = 13
end
object fcbDisplayMap: TscFontComboBox
Left = 176
Top = 245
Width = 209
Height = 24
DropDownCount = 12
Font.Charset = DEFAULT_CHARSET
Font.Color = clWindowText
Font.Height = -13
Font.Name = 'Tahoma'
Font.Style = []
ItemHeight = 18
ParentFont = False
TabOrder = 12
MoveUsedToTop = False
ColorUsed = clWindow
PreviewWidth = 240
PreviewHeight = 60
FontTypes = [ftTrueTypeAnsi, ftTrueTypeSymbol]
ShowPreview = False
ShowPreviewFontName = False
ShowPreviewInList = False
end
end

View file

@ -66,6 +66,10 @@ type
fcbTouchLayoutPhone: TscFontComboBox;
fcbTouchLayoutTablet: TscFontComboBox;
fcbTouchLayoutDesktop: TscFontComboBox;
lblDisplayMap: TLabel;
editDisplayMapSize: TEdit;
fcbDisplayMap: TscFontComboBox;
Label4: TLabel;
procedure FormCreate(Sender: TObject);
procedure cmdOKClick(Sender: TObject);
private
@ -120,6 +124,10 @@ begin
FControls[kfontTouchLayoutDesktop].CaptionLabel := lblTouchLayoutDesktopFont;
FControls[kfontTouchLayoutDesktop].NameCombo := fcbTouchLayoutDesktop;
FControls[kfontTouchLayoutDesktop].SizeEdit := editTouchLayoutDesktopSize;
FControls[kfontDisplayMap].CaptionLabel := lblDisplayMap;
FControls[kfontDisplayMap].NameCombo := fcbDisplayMap;
FControls[kfontDisplayMap].SizeEdit := editDisplayMapSize;
end;
function TfrmKeyboardFonts.GetFontInfo(Index: TKeyboardFont): TKeyboardFontInfo;

View file

@ -200,7 +200,7 @@ end;
function TkmnProjectFileUI.TestKeymanWeb(FSilent: Boolean): Boolean; // I4409
var
FCompiledName: string;
FCompiledName, OSKFont: string;
editor: TfrmTikeEditor;
wizard: TfrmKeymanWizard;
i: TKeyboardFont;
@ -250,7 +250,7 @@ begin
// We register all fonts that are used by the layout,
// but just once for each reference!
for i := kfontChar to kfontTouchLayoutDesktop do
for i := kfontChar to kfontDisplayMap do
begin
Found := False;
for j := kfontChar to TKeyboardFont(Ord(i)-1) do
@ -265,13 +265,18 @@ begin
if TServerUI.VerifyServerRunning then
begin
if Wizard.FontInfo[kfontDisplayMap].Enabled then
begin
OSKFont := Wizard.FontInfo[kfontDisplayMap].Name
end
else
OSKFont := Wizard.FontInfo[kfontOSK].Name;
TServerDebugAPI.RegisterKeyboard(
FCompiledName,
ProjectFile.FileVersion,
// We only need to specify the char + osk fonts here
// as the others are referenced in the touch layout definition directly
Wizard.FontInfo[kfontChar].Name,
Wizard.FontInfo[kfontOSK].Name
OSKFont
);
wizard.NotifyStartedWebDebug; // I4021

View file

@ -0,0 +1,15 @@
---
title: Gesture Hints and Tips
---
## Using Gestures on the Keyboard Keys
There are now several gestures available to interact with some keys on the keyboard. Keys which have gestures available will normally show them as a hint in the top right of the key:
![](../ios_images/gesture-hint.png)
* **Long press**: Press and hold a key, and after a moment a submenu will appear. Slide the finger to the submenu to select a key. Release the finger, and the highlighted key from the submenu will be output.
* **Flick**: Press and hold a key, and then slide the finger in various directions on a key to reach alternate outputs. The key will animate to show the expected output when the finger gets released. A common flick is to slide down to reach numerals on the top row of the keyboard.
An up-flick on keys that support longpresses but do not have a defined up-flick will skip the wait and immediately display their longpress submenu.
* **Multitap**: Some keys can be pressed repeatedly and rapidly to reach alternate outputs. Tapping rapidly twice on <kbd>Shift</kbd> will activate <kbd>Caps Lock</kbd> on many keyboards.

View file

@ -2,6 +2,8 @@
title: Keyman for iPhone and iPad Help
---
[Keyboard gestures](gestures)
| | Keyboard Keys <br/>(Keyboard Present) |
|---|---|
| ![](../ios_images/globe.png) | Install or select another language/keyboard |

View file

@ -18,6 +18,7 @@ title: Keyman for iPhone and iPad 18.0 Help
* [Using the Banner on the Keyboard](basic/using-the-banner)
* [Switching Between Keyboards](basic/switching-between-keyboards)
* [Using a Keyboard](basic/keyboard-usage)
* [Keyboard Gestures](context/gestures)
* [Using the Menu](context/)
* [Using the Settings Screen](basic/config/)
* [Language Settings](basic/language-settings)

Binary file not shown.

After

Width:  |  Height:  |  Size: 559 B

View file

@ -1,3 +1,9 @@
keyman (17.0.333-1) unstable; urgency=medium
* New upstream release.
-- Eberhard Beilharz <eb1@sil.org> Thu, 16 Jan 2025 16:29:44 +0100
keyman (17.0.332-1) unstable; urgency=medium
* set environment variable for rendering of downloads dialog (#12617)

View file

@ -4,24 +4,24 @@ Upstream-Contact: Keyman team <support@keyman.com>
Source: https://github.com/keymanapp/keyman
Files: *
Copyright: 2018-2024 SIL Global
Copyright: 2018-2025 SIL Global
License: MIT
Files: linux/ibus-keyman/*
Copyright: 2004-2024 SIL Global
Copyright: 2004-2025 SIL Global
License: GPL-2+
Files: linux/ibus-keyman/src/keymanutil.c
linux/ibus-keyman/src/keymanutil.h
linux/ibus-keyman/src/kmpdetails.c
linux/ibus-keyman/src/kmpdetails.h
Copyright: 2009-2024 SIL Global
Copyright: 2009-2025 SIL Global
License: GPL-2+ or MIT
Files: linux/ibus-keyman/src/keyman-service.c
linux/ibus-keyman/src/keyman-service.h
linux/keyman-config/buildtools/help2md
Copyright: 2018-2024 SIL Global
Copyright: 2018-2025 SIL Global
License: GPL-3+
Files: linux/ibus-keyman/tests/ibusimcontext.c
@ -31,7 +31,7 @@ Copyright: 2008-2010, Peng Huang <shawn.p.huang@gmail.com>
2008-2013, Peng Huang <shawn.p.huang@gmail.com>
2008-2021, Red Hat, Inc.
2015-2021, Takao Fujiwara <takao.fujiwara1@gmail.com>
2021-2024, SIL Global
2021-2025, SIL Global
License: LGPL-2.1+
Files: linux/keyman-config/buildtools/help2man
@ -41,7 +41,7 @@ License: GPL-3+
Files: debian/com.keyman.config.appdata.xml
debian/com.keyman.ibus_keyman.metainfo.xml
Copyright: 2019 Daniel Glassey <wdg@debian.org>
2022-2024 SIL Global
2022-2025 SIL Global
License: MIT
License: MIT

View file

@ -20,9 +20,12 @@
</script>
</head>
<body>
<h1>Automatic Mode Example</h1>
<form action='.' method='post'>
<p><input type='text' id='multilingual' name='multilingual' size="40" /></p>
<p><textarea cols='40' rows='5'></textarea></p>
</form>
<a href="automatic-control.md">Back to Document</a>
</body>
</html>

View file

@ -33,6 +33,7 @@
</script>
</head>
<body>
<h1>Control-by-Control Example</h1>
<form name='f' action='.' method='post'>
<p>Email to <span class='note'>(KeymanWeb not enabled)</span><br /><input type='text' name='address' size="40" placeholder="id = address"/></p>
@ -43,5 +44,6 @@
</form>
<a href="control-by-control.md">Back to Document</a>
</body>
</html>

View file

@ -18,6 +18,9 @@
</script>
</head>
<body>
<h1>First Example</h1>
<p>Click me and type: <input placeholder="Hello World"/></p>
<a href="index.md">Back to Document</a>
</body>
</html>

View file

@ -43,7 +43,7 @@ window.addEventListener( 'load' , SetupDocument );
</script>
</head>
<body>
<h1>Manual Control - Custom Interface</h1>
<form name='f' action='.' method='post'>
<p>Keyboard: <select id='KWControl' onchange='KWControlChange()'><option value=''>English</option></select></p>
@ -51,5 +51,6 @@ window.addEventListener( 'load' , SetupDocument );
<p><input type='text' id='multilingual' name='multilingual' size="40" placeholder="id='multilingual'"/></p>
</form>
<a href="full-manual-control.md">Back to Document</a>
</body>
</html>

View file

@ -32,7 +32,7 @@
</script>
</head>
<body>
<h1>Manual Mode Example</h1>
<form action='.' method='post'>
<p><img style="border: solid 1px black; padding: 2px 2px 3px 2px" src='js/kmicon.png' alt='KeymanWeb' onclick='KWControlClick()' id='KWControl' /></p>
@ -40,6 +40,7 @@
<p><input type='text' id='multilingual' name='multilingual' size="40" /></p>
<p><textarea cols='40' rows='5'></textarea></p>
<a href="manual-control.md">Back to Document</a>
</form>
</body>
</html>

View file

@ -8,6 +8,7 @@ import { distributionFromDistanceMaps } from '../../../corrections.js';
import Modipress from './modipress.js';
import { keySupportsModipress } from '../specsForLayout.js';
import { GesturePreviewHost } from '../../../keyboard-layout/gesturePreviewHost.js';
import { correctionKeyFilter } from '../../../correctionLayout.js';
/**
* Represents a potential multitap gesture's implementation within KeymanWeb.
@ -104,20 +105,33 @@ export default class Multitap implements GestureHandler {
keyEvent.baseTranscriptionToken = this.baseContextToken;
const coord = tap.sources[0].currentSample;
const baseDistances = vkbd.getSimpleTapCorrectionDistances(coord, this.baseKey.key.spec as ActiveKey);
const baseSpec = this.baseKey.key.spec;
const baseDistances = vkbd.getSimpleTapCorrectionDistances(coord, baseSpec as ActiveKey);
// If the key is on a different layer than is shown, what if the user meant to hit a different key?
// (Bypass if the tap is part of a modipress gesture.)
if(coord.stateToken != vkbd.layerId && !tap.matchedId.includes('modipress')) {
const matchKey = vkbd.layerGroup.findNearestKey({...coord, stateToken: vkbd.layerId});
// Replace the key at the current location for the current layer key
// with the multitap base key.
const p = baseDistances.get(matchKey.key.spec);
if(p == null) {
console.warn("Could not find current layer's key")
// That said, if they directly touched a non-output key, simplify and assume
// that they hit the intended key.
if(correctionKeyFilter(matchKey.key.spec)) {
// Replace the key at the current location for the current layer key
// with the multitap base key.
const p = baseDistances.get(matchKey.key.spec);
if(p == null) {
console.warn("Could not find current layer's key")
} else {
baseDistances.delete(matchKey.key.spec);
baseDistances.set(coord.item.key.spec, p);
}
}
baseDistances.delete(matchKey.key.spec);
baseDistances.set(coord.item.key.spec, p);
}
keyEvent.keyDistribution = this.currentStageKeyDistribution(baseDistances);
// Will also bypass if the base key is modipress-compatible.
if(coord.item && correctionKeyFilter(coord.item.key.spec)) {
keyEvent.keyDistribution = this.currentStageKeyDistribution(baseDistances);
}
// TODO for future: multitap previews.

View file

@ -161,15 +161,21 @@ describe("PredictionContext", () => {
assert.equal(updateFake.callCount, 3);
suggestions = updateFake.thirdCall.args[0];
// Note: this unit test was originally written with auto-correct on!
// #11941 was written 2024-07-25 (added unit test for auto-correction method)
// #12169 was written 2024-08-14, which is what added THIS unit test.
// This does re-use the apply-revert oriented mocking.
// Should skip the (second) "apple", "apply", "apps" round, as it became outdated
// by its following request before its response could be received.
assert.deepEqual(suggestions.map((obj) => obj.displayAs), ['“apple”', 'applied']);
assert.equal(suggestions.find((obj) => obj.tag == 'keep').displayAs, '“apple”');
assert.deepEqual(suggestions.map((obj) => obj.displayAs), ['applied']); // '“apple”' included with auto-correct enabled.
// Is not displayed; we only display it if auto-correct is on, as 'applied' would be automatic then.
assert.equal(predictiveContext.keepSuggestion.displayAs, '“apple”');
// assert.equal(suggestions.find((obj) => obj.tag == 'keep').displayAs, '“apple”'); // with auto-correct enabled.
assert.equal(suggestions.find((obj) => obj.transform.deleteLeft != 0).displayAs, 'applied');
// Our reused mocking doesn't directly provide the 'keep' suggestion; we
// need to remove it before testing for set equality.
assert.deepEqual(suggestions.splice(1), expected);
assert.deepEqual(suggestions /*.splice(1)*/, expected);
});
it('sendUpdateState retrieves the most recent suggestion set', async function() {

View file

@ -6,6 +6,5 @@ stateDiagram
Downloading --> Installing
Downloading --> WaitingRestart
WaitingRestart --> Installing
Installing --> PostInstall
PostInstall --> Idle
Installing --> Idle
```