mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-25 07:17:41 +00:00
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:
commit
521c78c5dd
56 changed files with 1288 additions and 106 deletions
44
HISTORY.md
44
HISTORY.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
18.0.169
|
||||
18.0.175
|
||||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
;
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
|
|
|||
498
common/web/types/tests/kmx/kmx-plus/element-string.tests.ts
Normal file
498
common/web/types/tests/kmx/kmx-plus/element-string.tests.ts
Normal 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));
|
||||
};
|
||||
|
||||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
@ -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>}
|
||||
38
developer/src/common/web/utils/src/valid-ids.ts
Normal file
38
developer/src/common/web/utils/src/valid-ids.ts
Normal 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);
|
||||
}
|
||||
76
developer/src/common/web/utils/test/valid-ids.tests.ts
Normal file
76
developer/src/common/web/utils/test/valid-ids.tests.ts
Normal 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`));
|
||||
});
|
||||
});
|
||||
|
|
@ -5,8 +5,12 @@
|
|||
"rootDir": ".",
|
||||
"outDir": "./build/",
|
||||
"baseUrl": ".",
|
||||
|
||||
/** for ttfmeta */
|
||||
"allowJs": true,
|
||||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/ttfmeta/lib/*.js",
|
||||
],
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,7 +54,8 @@
|
|||
"exclude-after-remap": true,
|
||||
"exclude": [
|
||||
"test/",
|
||||
"src/template/"
|
||||
"src/template/",
|
||||
"src/main.ts"
|
||||
]
|
||||
},
|
||||
"repository": {
|
||||
|
|
|
|||
|
|
@ -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`);
|
||||
|
|
|
|||
|
|
@ -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'.`
|
||||
);
|
||||
};
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
|||
26
developer/src/kmc-generate/test/fixtures/basic/welcome.htm
vendored
Normal file
26
developer/src/kmc-generate/test/fixtures/basic/welcome.htm
vendored
Normal 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>
|
||||
|
|
@ -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));
|
||||
});
|
||||
});
|
||||
|
|
@ -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" });
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -13,7 +13,6 @@
|
|||
},
|
||||
"include": [
|
||||
"src/**/*.ts",
|
||||
"src/ttfmeta/lib/*.js",
|
||||
"src/imports/langtags.js",
|
||||
],
|
||||
"references": [
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
16
developer/src/kmc-package/src/compiler/kvk-font-facename.ts
Normal file
16
developer/src/kmc-package/src/compiler/kvk-font-facename.ts
Normal 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);
|
||||
}
|
||||
|
|
@ -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.`
|
||||
);
|
||||
}
|
||||
|
||||
|
|
|
|||
BIN
developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx
vendored
Normal file
BIN
developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx
vendored
Normal file
Binary file not shown.
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
15
ios/docs/help/context/gestures.md
Normal file
15
ios/docs/help/context/gestures.md
Normal 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:
|
||||
|
||||

|
||||
|
||||
* **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.
|
||||
|
|
@ -2,6 +2,8 @@
|
|||
title: Keyman for iPhone and iPad Help
|
||||
---
|
||||
|
||||
[Keyboard gestures](gestures)
|
||||
|
||||
| | Keyboard Keys <br/>(Keyboard Present) |
|
||||
|---|---|
|
||||
|  | Install or select another language/keyboard |
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
BIN
ios/docs/help/ios_images/gesture-hint.png
Normal file
BIN
ios/docs/help/ios_images/gesture-hint.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 559 B |
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
@ -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>
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
|
|
@ -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.
|
||||
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,5 @@ stateDiagram
|
|||
Downloading --> Installing
|
||||
Downloading --> WaitingRestart
|
||||
WaitingRestart --> Installing
|
||||
Installing --> PostInstall
|
||||
PostInstall --> Idle
|
||||
Installing --> Idle
|
||||
```
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue