From fea40e2689b8f185568a2b42f6575dcae846077f Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 9 Dec 2024 11:47:54 +0000 Subject: [PATCH 01/52] chore(common/web): initial test boilerplate --- .../kmx/kmx-plus/element-string.tests.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 common/web/types/tests/kmx/kmx-plus/element-string.tests.ts diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts new file mode 100644 index 0000000000..438c16bf4a --- /dev/null +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -0,0 +1,19 @@ +/* + * 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'; + +describe('Test of ElementString', () => { + describe('Test of ElemElement', () => { + describe('Test of isEqual()', () => { + assert.isTrue(true); + assert.isFalse(false); + }); + }); +}); From 13b463aedf8ef29cd1e6356662b34b6242116c49 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 9 Dec 2024 16:16:31 +0000 Subject: [PATCH 02/52] chore(common/web): add initElemElement and first true test case for isEqual() --- .../kmx/kmx-plus/element-string.tests.ts | 32 +++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 438c16bf4a..2ced6327ca 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -8,12 +8,40 @@ import 'mocha'; import { assert } from 'chai'; +import { ElemElementFlags, ElemElement } from '../../../src/kmx/kmx-plus/element-string.js'; +import { StrsItem, UsetItem } from '../../../src/kmx/kmx-plus/kmx-plus.js'; +import { UnicodeSet } from '../../../src/ldml-keyboard/unicodeset-parser-api.js'; + +const GOTHIC_A = new StrsItem("𐌰", 0x10330); +const GOTHIC_A_SET = new UsetItem( + new UnicodeSet("[𐌰-𐍊]", [[0x10330,0x1034A]]), + GOTHIC_A +); describe('Test of ElementString', () => { describe('Test of ElemElement', () => { describe('Test of isEqual()', () => { - assert.isTrue(true); - assert.isFalse(false); + it('returns true when elems identical', () => { + const one = initElemElement(); + const two = initElemElement() + assert.isTrue(one.isEqual(two)); + }); }); }); }); + +function initElemElement( + value: StrsItem = GOTHIC_A, + uset: UsetItem = GOTHIC_A_SET, + order: number = 0, + tertiary: number = 1, + 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; +} From ee23da5647ad9c3b8faca036a85c94a3cae9ddef Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 16 Dec 2024 12:17:18 +0000 Subject: [PATCH 03/52] chore(common/web): add five test cases for ElemElement.isEqual() --- .../kmx/kmx-plus/element-string.tests.ts | 35 +++++++++++++++++-- 1 file changed, 33 insertions(+), 2 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 2ced6327ca..f1a530efde 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -13,10 +13,16 @@ import { StrsItem, UsetItem } from '../../../src/kmx/kmx-plus/kmx-plus.js'; import { UnicodeSet } from '../../../src/ldml-keyboard/unicodeset-parser-api.js'; const GOTHIC_A = new StrsItem("𐌰", 0x10330); -const GOTHIC_A_SET = new UsetItem( +const GOTHIC_B = new StrsItem("𐌱", 0x10331); +const UGARITIC_A = new StrsItem("𐎀", 0x10380); +const GOTHIC_SET = new UsetItem( new UnicodeSet("[𐌰-𐍊]", [[0x10330,0x1034A]]), GOTHIC_A ); +const UGARITIC_SET = new UsetItem( + new UnicodeSet("[𐎀-𐎟]", [[0x10380,0x1039F]]), + UGARITIC_A +); describe('Test of ElementString', () => { describe('Test of ElemElement', () => { @@ -26,13 +32,38 @@ describe('Test of ElementString', () => { const two = initElemElement() 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_SET, 0); + const two = initElemElement(GOTHIC_A, GOTHIC_SET, 1); + assert.isFalse(one.isEqual(two)); + }); + it('returns false when tertiary differs', () => { + const one = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 1); + const two = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 2); + assert.isFalse(one.isEqual(two)); + }); + it('returns false when flags differs', () => { + const one = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 1, ElemElementFlags.none); + const two = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 1, ElemElementFlags.type); + assert.isFalse(one.isEqual(two)); + }); + it('returns true even though uset differs', () => { + const one = initElemElement(GOTHIC_A, GOTHIC_SET); + const two = initElemElement(GOTHIC_A, UGARITIC_SET); + assert.isTrue(one.isEqual(two)); + }); }); }); }); function initElemElement( value: StrsItem = GOTHIC_A, - uset: UsetItem = GOTHIC_A_SET, + uset: UsetItem = GOTHIC_SET, order: number = 0, tertiary: number = 1, flags: ElemElementFlags = ElemElementFlags.none, From 77076eb9ed09157b66e8159cd83078f8b705e8c3 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 16 Dec 2024 12:43:53 +0000 Subject: [PATCH 04/52] chore(common/web): add first test case for ElementString() --- .../types/tests/kmx/kmx-plus/element-string.tests.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index f1a530efde..ce3db90fc3 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -8,7 +8,7 @@ import 'mocha'; import { assert } from 'chai'; -import { ElemElementFlags, ElemElement } from '../../../src/kmx/kmx-plus/element-string.js'; +import { ElemElementFlags, ElemElement, ElementString } from '../../../src/kmx/kmx-plus/element-string.js'; import { StrsItem, UsetItem } from '../../../src/kmx/kmx-plus/kmx-plus.js'; import { UnicodeSet } from '../../../src/ldml-keyboard/unicodeset-parser-api.js'; @@ -59,6 +59,14 @@ describe('Test of ElementString', () => { }); }); }); + describe('Test of ElementString', () => { + describe('Test of fromStrings()', () => { + it('returns an empty El;ementString if source is null', () => { + const es = ElementString.fromStrings({}, null); + assert.deepEqual(es, new ElementString()); + }); + }); + }); }); function initElemElement( From 703fd2cbe6ec2be2995d0c3bb6ae8be08ac480b9 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 16 Dec 2024 15:41:00 +0000 Subject: [PATCH 05/52] chore(common/web): add use case for string array --- .../kmx/kmx-plus/element-string.tests.ts | 28 +++++++++++++------ 1 file changed, 20 insertions(+), 8 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index ce3db90fc3..7c187d9920 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -9,11 +9,12 @@ import 'mocha'; import { assert } from 'chai'; import { ElemElementFlags, ElemElement, ElementString } from '../../../src/kmx/kmx-plus/element-string.js'; -import { StrsItem, UsetItem } from '../../../src/kmx/kmx-plus/kmx-plus.js'; +import { StrsItem, UsetItem, DependencySections, Strs } from '../../../src/kmx/kmx-plus/kmx-plus.js'; import { UnicodeSet } from '../../../src/ldml-keyboard/unicodeset-parser-api.js'; const GOTHIC_A = new StrsItem("𐌰", 0x10330); const GOTHIC_B = new StrsItem("𐌱", 0x10331); +const GOTHIC_C = new StrsItem("𐌲", 0x10332); const UGARITIC_A = new StrsItem("𐎀", 0x10380); const GOTHIC_SET = new UsetItem( new UnicodeSet("[𐌰-𐍊]", [[0x10330,0x1034A]]), @@ -43,13 +44,13 @@ describe('Test of ElementString', () => { assert.isFalse(one.isEqual(two)); }); it('returns false when tertiary differs', () => { - const one = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 1); - const two = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 2); + const one = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 0); + const two = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 1); assert.isFalse(one.isEqual(two)); }); it('returns false when flags differs', () => { - const one = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 1, ElemElementFlags.none); - const two = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 1, ElemElementFlags.type); + const one = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 0, ElemElementFlags.none); + const two = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 0, ElemElementFlags.type); assert.isFalse(one.isEqual(two)); }); it('returns true even though uset differs', () => { @@ -61,19 +62,30 @@ describe('Test of ElementString', () => { }); describe('Test of ElementString', () => { describe('Test of fromStrings()', () => { - it('returns an empty El;ementString if source is null', () => { + 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', () => { + const strs: Strs = new Strs(); + const sections: DependencySections = { strs: strs }; + const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + const expected = [ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]; + assert.deepEqual(actual, expected); + }); }); }); }); function initElemElement( value: StrsItem = GOTHIC_A, - uset: UsetItem = GOTHIC_SET, + uset: UsetItem = undefined, order: number = 0, - tertiary: number = 1, + tertiary: number = 0, flags: ElemElementFlags = ElemElementFlags.none, ): ElemElement { const ee = new ElemElement(); From 76316f1661bda4dd41f842d2eb0fd67b2be53cca Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 16 Dec 2024 17:07:52 +0000 Subject: [PATCH 06/52] chore(common/web): add test case for fromStrings() from string --- .../kmx/kmx-plus/element-string.tests.ts | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 7c187d9920..926fd8eff2 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -11,6 +11,7 @@ import { assert } from 'chai'; import { ElemElementFlags, ElemElement, ElementString } from '../../../src/kmx/kmx-plus/element-string.js'; import { StrsItem, UsetItem, DependencySections, Strs } from '../../../src/kmx/kmx-plus/kmx-plus.js'; import { UnicodeSet } 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); @@ -25,6 +26,8 @@ const UGARITIC_SET = new UsetItem( UGARITIC_A ); +let origElementParserSegment = ElementParser.segment; + describe('Test of ElementString', () => { describe('Test of ElemElement', () => { describe('Test of isEqual()', () => { @@ -62,6 +65,14 @@ describe('Test of ElementString', () => { }); describe('Test of ElementString', () => { describe('Test of fromStrings()', () => { + beforeEach(() => { + ElementParser.segment = (str: string): ElementSegment[] => { + return [...str].map(s => new ElementSegment(s, ElementType.codepoint)); + } + }); + afterEach(() => { + ElementParser.segment = origElementParserSegment; + }); it('returns an empty ElementString if source is null', () => { const es = ElementString.fromStrings({}, null); assert.deepEqual(es, new ElementString()); @@ -78,6 +89,17 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); }); + it('can create an ElementString from a string', () => { + const strs: Strs = new Strs(); + const sections: DependencySections = { strs: strs }; + const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲"); + const expected = [ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]; + assert.deepEqual(actual, expected); + }); }); }); From 09168fc083875f2d16faeb749863a2430a322a35 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 16 Dec 2024 17:38:32 +0000 Subject: [PATCH 07/52] chore(common/web): remove unneeded local variables --- .../types/tests/kmx/kmx-plus/element-string.tests.ts | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 926fd8eff2..74207775a7 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -9,7 +9,7 @@ import 'mocha'; import { assert } from 'chai'; import { ElemElementFlags, ElemElement, ElementString } from '../../../src/kmx/kmx-plus/element-string.js'; -import { StrsItem, UsetItem, DependencySections, Strs } from '../../../src/kmx/kmx-plus/kmx-plus.js'; +import { StrsItem, UsetItem, Strs } from '../../../src/kmx/kmx-plus/kmx-plus.js'; import { UnicodeSet } from '../../../src/ldml-keyboard/unicodeset-parser-api.js'; import { ElementParser, ElementSegment, ElementType } from '../../../src/ldml-keyboard/pattern-parser.js'; @@ -78,9 +78,7 @@ describe('Test of ElementString', () => { assert.deepEqual(es, new ElementString()); }); it('can create an ElementString from a string array', () => { - const strs: Strs = new Strs(); - const sections: DependencySections = { strs: strs }; - const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + const actual = ElementString.fromStrings({ strs: new Strs() }, ["𐌰", "𐌱", "𐌲"]); const expected = [ initElemElement(GOTHIC_A), initElemElement(GOTHIC_B), @@ -90,9 +88,7 @@ describe('Test of ElementString', () => { }); }); it('can create an ElementString from a string', () => { - const strs: Strs = new Strs(); - const sections: DependencySections = { strs: strs }; - const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲"); + const actual = ElementString.fromStrings({ strs: new Strs() }, "𐌰𐌱𐌲"); const expected = [ initElemElement(GOTHIC_A), initElemElement(GOTHIC_B), From 5e4d699a7258082311d70ce34ea6df75dd97440f Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 16 Dec 2024 17:51:29 +0000 Subject: [PATCH 08/52] chore(common/web): add four test cases for order andf tertiary arguments to fromStrings() --- .../kmx/kmx-plus/element-string.tests.ts | 54 +++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 74207775a7..21914a0aa0 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -96,6 +96,60 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it('can apply order string', () => { + const actual = ElementString.fromStrings( + { strs: new Strs() }, + "𐌰𐌱𐌲", + "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', () => { + const actual = ElementString.fromStrings( + { strs: new Strs() }, + "𐌰𐌱𐌲", + "1", + ); + const expected = [ + initElemElement(GOTHIC_A, undefined, 1), + initElemElement(GOTHIC_B, undefined, 1), + initElemElement(GOTHIC_C, undefined, 1), + ]; + assert.deepEqual(actual, expected); + }); + it('can apply tertiary string', () => { + const actual = ElementString.fromStrings( + { strs: new Strs() }, + "𐌰𐌱𐌲", + 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', () => { + const actual = ElementString.fromStrings( + { strs: new Strs() }, + "𐌰𐌱𐌲", + 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); + }); }); }); From aef4ef41f03bbd0087951a10af2bab82253ae07c Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 16 Dec 2024 18:04:43 +0000 Subject: [PATCH 09/52] chore(common/web): add four test cases for tertiary_base and prebase arguments to fromStrings() --- .../kmx/kmx-plus/element-string.tests.ts | 62 +++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 21914a0aa0..a7ab25ee7a 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -150,6 +150,68 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it('can apply tertiary_base string', () => { + const actual = ElementString.fromStrings( + { strs: new Strs() }, + "𐌰𐌱𐌲", + 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', () => { + const actual = ElementString.fromStrings( + { strs: new Strs() }, + "𐌰𐌱𐌲", + 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 apply prebase string', () => { + const actual = ElementString.fromStrings( + { strs: new Strs() }, + "𐌰𐌱𐌲", + 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', () => { + const actual = ElementString.fromStrings( + { strs: new Strs() }, + "𐌰𐌱𐌲", + 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); + }); }); }); From e6abbf027fce302e5923218a8d493f1e86524b4a Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 19 Dec 2024 12:00:33 +0000 Subject: [PATCH 10/52] chore(common/web): stub out strs.allocString() --- .../kmx/kmx-plus/element-string.tests.ts | 49 ++++++++++++++----- 1 file changed, 37 insertions(+), 12 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index a7ab25ee7a..dd0aae7a9b 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -9,7 +9,7 @@ import 'mocha'; import { assert } from 'chai'; import { ElemElementFlags, ElemElement, ElementString } from '../../../src/kmx/kmx-plus/element-string.js'; -import { StrsItem, UsetItem, Strs } from '../../../src/kmx/kmx-plus/kmx-plus.js'; +import { StrsItem, UsetItem, Strs, StrsOptions, DependencySections, CharStrsItem } from '../../../src/kmx/kmx-plus/kmx-plus.js'; import { UnicodeSet } from '../../../src/ldml-keyboard/unicodeset-parser-api.js'; import { ElementParser, ElementSegment, ElementType } from '../../../src/ldml-keyboard/pattern-parser.js'; @@ -78,7 +78,9 @@ describe('Test of ElementString', () => { assert.deepEqual(es, new ElementString()); }); it('can create an ElementString from a string array', () => { - const actual = ElementString.fromStrings({ strs: new Strs() }, ["𐌰", "𐌱", "𐌲"]); + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); const expected = [ initElemElement(GOTHIC_A), initElemElement(GOTHIC_B), @@ -88,7 +90,9 @@ describe('Test of ElementString', () => { }); }); it('can create an ElementString from a string', () => { - const actual = ElementString.fromStrings({ strs: new Strs() }, "𐌰𐌱𐌲"); + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; + const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲"); const expected = [ initElemElement(GOTHIC_A), initElemElement(GOTHIC_B), @@ -97,8 +101,10 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can apply order string', () => { + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( - { strs: new Strs() }, + sections, "𐌰𐌱𐌲", "1 2 3", ); @@ -110,8 +116,10 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can apply single order to all', () => { + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( - { strs: new Strs() }, + sections, "𐌰𐌱𐌲", "1", ); @@ -123,8 +131,10 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can apply tertiary string', () => { + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( - { strs: new Strs() }, + sections, "𐌰𐌱𐌲", null, "1 2 3", @@ -137,8 +147,10 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can apply single tertiary to all', () => { + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( - { strs: new Strs() }, + sections, "𐌰𐌱𐌲", null, "1", @@ -151,8 +163,10 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can apply tertiary_base string', () => { + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( - { strs: new Strs() }, + sections, "𐌰𐌱𐌲", null, null, @@ -166,8 +180,10 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can apply single tertiary_base to all', () => { + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( - { strs: new Strs() }, + sections, "𐌰𐌱𐌲", null, null, @@ -181,8 +197,10 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can apply prebase string', () => { + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( - { strs: new Strs() }, + sections, "𐌰𐌱𐌲", null, null, @@ -197,8 +215,10 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can apply single prebase to all', () => { + const sections = { strs: new Strs() }; + sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( - { strs: new Strs() }, + sections, "𐌰𐌱𐌲", null, null, @@ -229,4 +249,9 @@ function initElemElement( ee.tertiary = tertiary; ee.flags = flags; return ee; -} +}; + +function stubStrsAllocString_Char(s?: string, opts?: StrsOptions, sections?: DependencySections): StrsItem { + return new CharStrsItem(s); +}; + From 0585b3f639af14e5e6b98555d6e74eaf42ccc78f Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 19 Dec 2024 12:13:46 +0000 Subject: [PATCH 11/52] chore(common/web): add four test cases for order etc strings that are too short --- .../kmx/kmx-plus/element-string.tests.ts | 66 +++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index dd0aae7a9b..55e5f20e28 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -130,6 +130,21 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it.skip('can handle order string that is too short', () => { + const sections = { strs: new Strs() }; + 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 apply tertiary string', () => { const sections = { strs: new Strs() }; sections.strs.allocString = stubStrsAllocString_Char; @@ -162,6 +177,22 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it.skip('can handle tertiary string that is too short', () => { + const sections = { strs: new Strs() }; + 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 apply tertiary_base string', () => { const sections = { strs: new Strs() }; sections.strs.allocString = stubStrsAllocString_Char; @@ -196,6 +227,23 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it('can handle tertiary_base string that is too short', () => { + const sections = { strs: new Strs() }; + 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', () => { const sections = { strs: new Strs() }; sections.strs.allocString = stubStrsAllocString_Char; @@ -232,6 +280,24 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it('can handle prebase string that is too short', () => { + const sections = { strs: new Strs() }; + 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); + }); }); }); From 1ae53a042bb8beb6f93db09e6f159120a710c2e2 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 19 Dec 2024 14:43:15 +0000 Subject: [PATCH 12/52] chore(common/web): add test cases for non-number in order and tertiary strings --- .../kmx/kmx-plus/element-string.tests.ts | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 55e5f20e28..8df97fcc29 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -145,6 +145,21 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it.skip('can handle non-number in order string', () => { + const sections = { strs: new Strs() }; + 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', () => { const sections = { strs: new Strs() }; sections.strs.allocString = stubStrsAllocString_Char; @@ -193,6 +208,22 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it.skip('can handle non-number in tertiary string', () => { + const sections = { strs: new Strs() }; + 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', () => { const sections = { strs: new Strs() }; sections.strs.allocString = stubStrsAllocString_Char; From 7694623fc9f6cf283eaa04b0801e6b6aeab2b77c Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 19 Dec 2024 16:19:21 +0000 Subject: [PATCH 13/52] chore(common/web): add first uset test case with supporting stubs and TestUnicodeSetParser class --- .../kmx/kmx-plus/element-string.tests.ts | 84 ++++++++++++++----- 1 file changed, 63 insertions(+), 21 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 8df97fcc29..b9a5987109 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -8,25 +8,36 @@ 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 } from '../../../src/kmx/kmx-plus/kmx-plus.js'; -import { UnicodeSet } from '../../../src/ldml-keyboard/unicodeset-parser-api.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 UGARITIC_A = new StrsItem("𐎀", 0x10380); -const GOTHIC_SET = new UsetItem( - new UnicodeSet("[𐌰-𐍊]", [[0x10330,0x1034A]]), - GOTHIC_A +const GOTHIC_PATTERN = "[𐌰-𐍊]"; +const GOTHIC_STRSITEM = new StrsItem(GOTHIC_PATTERN); +const GOTHIC_USETITEM = new UsetItem( + new UnicodeSet(GOTHIC_PATTERN, [[0x10330,0x1034A]]), + GOTHIC_STRSITEM ); -const UGARITIC_SET = new UsetItem( - new UnicodeSet("[𐎀-𐎟]", [[0x10380,0x1039F]]), - UGARITIC_A +const UGARITIC_PATTERN = "[𐎀-𐎟]"; +const UGARITIC_STRSITEM = new StrsItem(UGARITIC_PATTERN); +const UGARITIC_USETITEM = new UsetItem( + new UnicodeSet(UGARITIC_PATTERN, [[0x10380,0x1039F]]), + UGARITIC_STRSITEM ); -let origElementParserSegment = ElementParser.segment; +class TestUnicodeSetParser implements UnicodeSetParser { + parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => { + return new UnicodeSet("[𐌰-𐍊]", [[0x10330,0x1034A]]); + }; + sizeUnicodeSet = (pattern: string) : number => { return 1; } +}; + +const origElementParserSegment = ElementParser.segment; describe('Test of ElementString', () => { describe('Test of ElemElement', () => { @@ -42,23 +53,23 @@ describe('Test of ElementString', () => { assert.isFalse(one.isEqual(two)); }); it('returns false when order differs', () => { - const one = initElemElement(GOTHIC_A, GOTHIC_SET, 0); - const two = initElemElement(GOTHIC_A, GOTHIC_SET, 1); + 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_SET, 0, 0); - const two = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 1); + 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_SET, 0, 0, ElemElementFlags.none); - const two = initElemElement(GOTHIC_A, GOTHIC_SET, 0, 0, ElemElementFlags.type); + 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_SET); - const two = initElemElement(GOTHIC_A, UGARITIC_SET); + const one = initElemElement(GOTHIC_A, GOTHIC_USETITEM); + const two = initElemElement(GOTHIC_A, UGARITIC_USETITEM); assert.isTrue(one.isEqual(two)); }); }); @@ -66,9 +77,7 @@ describe('Test of ElementString', () => { describe('Test of ElementString', () => { describe('Test of fromStrings()', () => { beforeEach(() => { - ElementParser.segment = (str: string): ElementSegment[] => { - return [...str].map(s => new ElementSegment(s, ElementType.codepoint)); - } + ElementParser.segment = stubElementParserSegment_CodePoint; }); afterEach(() => { ElementParser.segment = origElementParserSegment; @@ -329,6 +338,27 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); + it('can create an ElementString from a uset string', () => { + ElementParser.segment = origElementParserSegment; + const sections = { + strs: new Strs(), + uset: new Uset(), + usetparser: new TestUnicodeSetParser(), + }; + 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); + }); }); }); @@ -352,3 +382,15 @@ function stubStrsAllocString_Char(s?: string, opts?: StrsOptions, sections?: Dep 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 stubUsetAllocUset(set: UnicodeSet, sections: DependencySections) : UsetItem { + return new UsetItem(set, new StrsItem(set.pattern)); +}; + From 9569ef208c13cbd42688b420c534fc4db743ccb6 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 19 Dec 2024 17:11:15 +0000 Subject: [PATCH 14/52] chore(common/web): corrected nesting error, and moved more code into fixtures --- .../kmx/kmx-plus/element-string.tests.ts | 525 +++++++++--------- 1 file changed, 252 insertions(+), 273 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index b9a5987109..e5702ff072 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -19,25 +19,20 @@ const GOTHIC_B = new StrsItem("𐌱", 0x10331); const GOTHIC_C = new StrsItem("𐌲", 0x10332); const GOTHIC_PATTERN = "[𐌰-𐍊]"; const GOTHIC_STRSITEM = new StrsItem(GOTHIC_PATTERN); -const GOTHIC_USETITEM = new UsetItem( - new UnicodeSet(GOTHIC_PATTERN, [[0x10330,0x1034A]]), - GOTHIC_STRSITEM -); +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_USETITEM = new UsetItem( - new UnicodeSet(UGARITIC_PATTERN, [[0x10380,0x1039F]]), - UGARITIC_STRSITEM -); +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 => { - return new UnicodeSet("[𐌰-𐍊]", [[0x10330,0x1034A]]); - }; + parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => { return GOTHIC_SET; }; sizeUnicodeSet = (pattern: string) : number => { return 1; } }; const origElementParserSegment = ElementParser.segment; +let sections: DependencySections = null; describe('Test of ElementString', () => { describe('Test of ElemElement', () => { @@ -77,6 +72,11 @@ describe('Test of ElementString', () => { describe('Test of ElementString', () => { describe('Test of fromStrings()', () => { beforeEach(() => { + sections = { + strs: new Strs(), + uset: new Uset(), + usetparser: new TestUnicodeSetParser(), + }; ElementParser.segment = stubElementParserSegment_CodePoint; }); afterEach(() => { @@ -87,7 +87,6 @@ describe('Test of ElementString', () => { assert.deepEqual(es, new ElementString()); }); it('can create an ElementString from a string array', () => { - const sections = { strs: new Strs() }; sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); const expected = [ @@ -97,267 +96,247 @@ describe('Test of ElementString', () => { ]; assert.deepEqual(actual, expected); }); - }); - it('can create an ElementString from a string', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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.skip('can handle order string that is too short', () => { - const sections = { strs: new Strs() }; - 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.skip('can handle non-number in order string', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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.skip('can handle tertiary string that is too short', () => { - const sections = { strs: new Strs() }; - 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.skip('can handle non-number in tertiary string', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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', () => { - const sections = { strs: new Strs() }; - 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 = origElementParserSegment; - const sections = { - strs: new Strs(), - uset: new Uset(), - usetparser: new TestUnicodeSetParser(), - }; - 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('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.skip('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.skip('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.skip('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.skip('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 = origElementParserSegment; + 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); + }); }); }); }); From 58e5f0fe94c6f5d1bc0bc6369d82884514ae6fb8 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 19 Dec 2024 17:21:05 +0000 Subject: [PATCH 15/52] chore(common/web): stub the ElementParser.segment for uset --- common/web/types/tests/kmx/kmx-plus/element-string.tests.ts | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index e5702ff072..62350510ea 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -322,7 +322,7 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); it('can create an ElementString from a uset string', () => { - ElementParser.segment = origElementParserSegment; + ElementParser.segment = stubElementParserSegment_Uset; sections.strs.allocString = stubStrsAllocString_Str; sections.uset.allocUset = stubUsetAllocUset; const actual = ElementString.fromStrings(sections, "[𐌰-𐍊]"); @@ -369,6 +369,10 @@ 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 stubUsetAllocUset(set: UnicodeSet, sections: DependencySections) : UsetItem { return new UsetItem(set, new StrsItem(set.pattern)); }; From e169ece9a374a2f2ae25b26bada64a103acd3872 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 19 Dec 2024 17:32:42 +0000 Subject: [PATCH 16/52] chore(common/web): add two test cases for uset parse failures --- .../tests/kmx/kmx-plus/element-string.tests.ts | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 62350510ea..de18758578 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -27,8 +27,8 @@ 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 => { return GOTHIC_SET; }; - sizeUnicodeSet = (pattern: string) : number => { return 1; } + parseUnicodeSet = (pattern: string, rangeCount: number) : UnicodeSet | null => GOTHIC_SET; + sizeUnicodeSet = (pattern: string) : number => 1; }; const origElementParserSegment = ElementParser.segment; @@ -337,6 +337,16 @@ describe('Test of ElementString', () => { ]; 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, "[𐌰-𐍊]")); + }); }); }); }); From 80d2f498246d3274e338d6f43c9c15d518f12916 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 6 Jan 2025 15:50:07 +0000 Subject: [PATCH 17/52] chore(common/web): add test case for quad string in ElementString.fromStrings() --- .../tests/kmx/kmx-plus/element-string.tests.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index de18758578..3e53146989 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -17,6 +17,8 @@ import { ElementParser, ElementSegment, ElementType } from '../../../src/ldml-ke const GOTHIC_A = new StrsItem("𐌰", 0x10330); const GOTHIC_B = new StrsItem("𐌱", 0x10331); const GOTHIC_C = new StrsItem("𐌲", 0x10332); +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]]); @@ -347,6 +349,16 @@ describe('Test of ElementString', () => { 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); + }); }); }); }); @@ -383,6 +395,11 @@ 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)); }; From c615f82db2d2dfcc817d03ad9ce34ba1fbb6ba32 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 6 Jan 2025 16:41:36 +0000 Subject: [PATCH 18/52] chore(common/web): add test cases for ElemElement.isEqual() and ElementStrings.isEqual() --- .../kmx/kmx-plus/element-string.tests.ts | 40 +++++++++++++------ 1 file changed, 28 insertions(+), 12 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 3e53146989..db3fdcc535 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -41,7 +41,12 @@ describe('Test of ElementString', () => { describe('Test of isEqual()', () => { it('returns true when elems identical', () => { const one = initElemElement(); - const two = initElemElement() + const two = initElemElement(); + assert.isTrue(one.isEqual(two)); + }); + it.skip('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', () => { @@ -72,18 +77,18 @@ describe('Test of ElementString', () => { }); }); describe('Test of ElementString', () => { + beforeEach(() => { + sections = { + strs: new Strs(), + uset: new Uset(), + usetparser: new TestUnicodeSetParser(), + }; + ElementParser.segment = stubElementParserSegment_CodePoint; + }); + afterEach(() => { + ElementParser.segment = origElementParserSegment; + }); describe('Test of fromStrings()', () => { - beforeEach(() => { - sections = { - strs: new Strs(), - uset: new Uset(), - usetparser: new TestUnicodeSetParser(), - }; - ElementParser.segment = stubElementParserSegment_CodePoint; - }); - afterEach(() => { - ElementParser.segment = origElementParserSegment; - }); it('returns an empty ElementString if source is null', () => { const es = ElementString.fromStrings({}, null); assert.deepEqual(es, new ElementString()); @@ -360,6 +365,17 @@ describe('Test of ElementString', () => { assert.deepEqual(actual, expected); }); }); + describe('Test of isEqual()', () => { + it('returns true when ElementStrings are identical', () => { + const es = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + assert.isTrue(es.isEqual(es)); + }); + it.skip('returns true when ElementStrings are clones', () => { + const one = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + const two = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + assert.isTrue(one.isEqual(two)); + }); + }); }); }); From 704486ae2e91c7d2cf8aafd3198aa18c630bd241 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 9 Jan 2025 11:27:52 +0000 Subject: [PATCH 19/52] chore(common/web): add two more test cases for ElementString.isEqual() --- .../types/tests/kmx/kmx-plus/element-string.tests.ts | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index db3fdcc535..5c8f5ce80b 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -375,6 +375,16 @@ describe('Test of ElementString', () => { const two = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); assert.isTrue(one.isEqual(two)); }); + it('returns false when ElementStrings are different lengths', () => { + const one = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + const two = ElementString.fromStrings(sections, ["𐌰", "𐌱"]); + assert.isFalse(one.isEqual(two)); + }); + it('returns false when ElementStrings have different ElemElements', () => { + const one = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + const two = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌳"]); + assert.isFalse(one.isEqual(two)); + }); }); }); }); From 86095d4b11493d038c794235118f67bec13803de Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 9 Jan 2025 12:03:51 +0000 Subject: [PATCH 20/52] chore(common/web): add test case for string ElemElement --- .../tests/kmx/kmx-plus/element-string.tests.ts | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 5c8f5ce80b..3d20c7ea3d 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -364,6 +364,20 @@ describe('Test of ElementString', () => { ]; 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', () => { From 23b3de14d91ebfe19445cf2d3b8e1f3a6220fe6f Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 9 Jan 2025 12:08:15 +0000 Subject: [PATCH 21/52] chore(common/web): clarify describe strings --- common/web/types/tests/kmx/kmx-plus/element-string.tests.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 3d20c7ea3d..6c6f8b8ec9 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -36,7 +36,7 @@ class TestUnicodeSetParser implements UnicodeSetParser { const origElementParserSegment = ElementParser.segment; let sections: DependencySections = null; -describe('Test of ElementString', () => { +describe('Test of ElementString file', () => { describe('Test of ElemElement', () => { describe('Test of isEqual()', () => { it('returns true when elems identical', () => { @@ -76,7 +76,7 @@ describe('Test of ElementString', () => { }); }); }); - describe('Test of ElementString', () => { + describe('Test of ElementString class', () => { beforeEach(() => { sections = { strs: new Strs(), From 9d555a0639c24cb12a76863426b788063f04b367 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 9 Jan 2025 14:12:56 +0000 Subject: [PATCH 22/52] chore(common/web): introduce initElementString() to remove dependency on fromStrings() in isEqual() tests --- .../kmx/kmx-plus/element-string.tests.ts | 48 ++++++++++++++++--- 1 file changed, 41 insertions(+), 7 deletions(-) diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 6c6f8b8ec9..054bb3d393 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -17,6 +17,7 @@ import { ElementParser, ElementSegment, ElementType } from '../../../src/ldml-ke 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 = "[𐌰-𐍊]"; @@ -381,22 +382,49 @@ describe('Test of ElementString file', () => { }); describe('Test of isEqual()', () => { it('returns true when ElementStrings are identical', () => { - const es = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + const es = initElementString([ + initElemElement(GOTHIC_A), + initElemElement(GOTHIC_B), + initElemElement(GOTHIC_C), + ]); assert.isTrue(es.isEqual(es)); }); it.skip('returns true when ElementStrings are clones', () => { - const one = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); - const two = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); + 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 = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); - const two = ElementString.fromStrings(sections, ["𐌰", "𐌱"]); + 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 = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]); - const two = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌳"]); + 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)); }); }); @@ -419,6 +447,12 @@ function initElemElement( 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); }; From b9c7835fbf513b4e926146e81988a9e2f2df9f35 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Thu, 9 Jan 2025 14:45:35 +0000 Subject: [PATCH 23/52] fix(common/web): add isEqual() method to StrsItem and use it in ElemElement.isEqual() --- common/web/types/src/kmx/kmx-plus/element-string.ts | 4 ++-- common/web/types/src/kmx/kmx-plus/kmx-plus.ts | 4 ++++ common/web/types/tests/kmx/kmx-plus/element-string.tests.ts | 4 ++-- 3 files changed, 8 insertions(+), 4 deletions(-) diff --git a/common/web/types/src/kmx/kmx-plus/element-string.ts b/common/web/types/src/kmx/kmx-plus/element-string.ts index b389107cce..ad3eaf6084 100644 --- a/common/web/types/src/kmx/kmx-plus/element-string.ts +++ b/common/web/types/src/kmx/kmx-plus/element-string.ts @@ -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; diff --git a/common/web/types/src/kmx/kmx-plus/kmx-plus.ts b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts index a88688f7ab..d0de2989d0 100644 --- a/common/web/types/src/kmx/kmx-plus/kmx-plus.ts +++ b/common/web/types/src/kmx/kmx-plus/kmx-plus.ts @@ -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; + } }; /** diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 054bb3d393..0dc21a49fe 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -45,7 +45,7 @@ describe('Test of ElementString file', () => { const two = initElemElement(); assert.isTrue(one.isEqual(two)); }); - it.skip('returns true when elems are clones', () => { + it('returns true when elems are clones', () => { const one = initElemElement(new StrsItem("𐌰", 0x10330)); const two = initElemElement(new StrsItem("𐌰", 0x10330)); assert.isTrue(one.isEqual(two)); @@ -389,7 +389,7 @@ describe('Test of ElementString file', () => { ]); assert.isTrue(es.isEqual(es)); }); - it.skip('returns true when ElementStrings are clones', () => { + it('returns true when ElementStrings are clones', () => { const one = initElementString([ initElemElement(GOTHIC_A), initElemElement(GOTHIC_B), From a279767accf20966288b6bca746ac78548c6bc70 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 13 Jan 2025 11:37:58 +0000 Subject: [PATCH 24/52] fix(common/web): add ElementString.parseIntOrZero() method to handle invalid order and tertiary params --- common/web/types/src/kmx/kmx-plus/element-string.ts | 8 ++++++-- .../web/types/tests/kmx/kmx-plus/element-string.tests.ts | 8 ++++---- 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/common/web/types/src/kmx/kmx-plus/element-string.ts b/common/web/types/src/kmx/kmx-plus/element-string.ts index b389107cce..2b4321abda 100644 --- a/common/web/types/src/kmx/kmx-plus/element-string.ts +++ b/common/web/types/src/kmx/kmx-plus/element-string.ts @@ -104,8 +104,8 @@ export class ElementString extends Array { 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 { } return true; } + private static parseIntOrZero(str: string) { + const num = parseInt(str, 10); + return !Number.isNaN(num) ? num : 0; + } } ; diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index 054bb3d393..be8b56da2d 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -142,7 +142,7 @@ describe('Test of ElementString file', () => { ]; assert.deepEqual(actual, expected); }); - it.skip('can handle order string that is too short', () => { + it('can handle order string that is too short', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, @@ -156,7 +156,7 @@ describe('Test of ElementString file', () => { ]; assert.deepEqual(actual, expected); }); - it.skip('can handle non-number in order string', () => { + it('can handle non-number in order string', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, @@ -200,7 +200,7 @@ describe('Test of ElementString file', () => { ]; assert.deepEqual(actual, expected); }); - it.skip('can handle tertiary string that is too short', () => { + it('can handle tertiary string that is too short', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, @@ -215,7 +215,7 @@ describe('Test of ElementString file', () => { ]; assert.deepEqual(actual, expected); }); - it.skip('can handle non-number in tertiary string', () => { + it('can handle non-number in tertiary string', () => { sections.strs.allocString = stubStrsAllocString_Char; const actual = ElementString.fromStrings( sections, From d48ccda140f1c011af1b5f4edc9889d2e5266ea6 Mon Sep 17 00:00:00 2001 From: "Dr Mark C. Sinclair" Date: Mon, 13 Jan 2025 12:05:39 +0000 Subject: [PATCH 25/52] fix(common/web): add test cases for parseIntOrZero() --- .../types/src/kmx/kmx-plus/element-string.ts | 2 +- .../tests/kmx/kmx-plus/element-string.tests.ts | 18 ++++++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/common/web/types/src/kmx/kmx-plus/element-string.ts b/common/web/types/src/kmx/kmx-plus/element-string.ts index 2b4321abda..7a65592c0b 100644 --- a/common/web/types/src/kmx/kmx-plus/element-string.ts +++ b/common/web/types/src/kmx/kmx-plus/element-string.ts @@ -125,7 +125,7 @@ export class ElementString extends Array { } return true; } - private static parseIntOrZero(str: string) { + private static parseIntOrZero(str: string): number { const num = parseInt(str, 10); return !Number.isNaN(num) ? num : 0; } diff --git a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts index be8b56da2d..5f5b53481b 100644 --- a/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts +++ b/common/web/types/tests/kmx/kmx-plus/element-string.tests.ts @@ -428,6 +428,24 @@ describe('Test of ElementString file', () => { 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); + }); + }); }); }); From afd942f4a6d099955ae475387e06e949ed4b59e0 Mon Sep 17 00:00:00 2001 From: "Joshua A. Horton" Date: Wed, 15 Jan 2025 15:20:13 +0700 Subject: [PATCH 26/52] docs(ios): document gestures within iOS app help --- ios/docs/help/context/gestures.md | 15 +++++++++++++++ ios/docs/help/context/index.md | 2 ++ ios/docs/help/index.md | 1 + ios/docs/help/ios_images/gesture-hint.png | Bin 0 -> 559 bytes 4 files changed, 18 insertions(+) create mode 100644 ios/docs/help/context/gestures.md create mode 100644 ios/docs/help/ios_images/gesture-hint.png diff --git a/ios/docs/help/context/gestures.md b/ios/docs/help/context/gestures.md new file mode 100644 index 0000000000..251de0f1ce --- /dev/null +++ b/ios/docs/help/context/gestures.md @@ -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 Shift will activate Caps Lock on many keyboards. \ No newline at end of file diff --git a/ios/docs/help/context/index.md b/ios/docs/help/context/index.md index 99e41dd65c..90cfbfd5f6 100644 --- a/ios/docs/help/context/index.md +++ b/ios/docs/help/context/index.md @@ -2,6 +2,8 @@ title: Keyman for iPhone and iPad Help --- +[Keyboard gestures](gestures) + | | Keyboard Keys
(Keyboard Present) | |---|---| | ![](../ios_images/globe.png) | Install or select another language/keyboard | diff --git a/ios/docs/help/index.md b/ios/docs/help/index.md index c71c703e72..994bbbda9e 100644 --- a/ios/docs/help/index.md +++ b/ios/docs/help/index.md @@ -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) diff --git a/ios/docs/help/ios_images/gesture-hint.png b/ios/docs/help/ios_images/gesture-hint.png new file mode 100644 index 0000000000000000000000000000000000000000..155ba674185a2d57fbda35de07e3d2f7d39cbc32 GIT binary patch literal 559 zcmeAS@N?(olHy`uVBq!ia0vp^dO#e+!2~3iSQp*`QjEnx?oJHr&dIz4a#+$GeH|GX zHuiJ>Nn{1`ISV`@iy0V%NV7srqc=eM^`dod+S9Q*iA;i$0OrH^Z- zES=%G)mc-*>!6NP*TtOphc3p+{){%wH-=5gT8XA_WGDEY4%)(Y+RcLN z+=}+SY?n$SyUt}Wm#@y!6`8i~bg)m>vL&D1{*K#!zxst(aP?&EhbNYm)TU|vd0ATj z Date: Thu, 16 Jan 2025 16:29:53 +0100 Subject: [PATCH 27/52] chore(linux): Update debian changelog (cherry picked from commit fdafd5879ec926b1f71a768f61fe17e71749602b) --- linux/debian/changelog | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/linux/debian/changelog b/linux/debian/changelog index 3a2dae55f3..22fd40caad 100644 --- a/linux/debian/changelog +++ b/linux/debian/changelog @@ -1,3 +1,9 @@ +keyman (17.0.333-1) unstable; urgency=medium + + * New upstream release. + + -- Eberhard Beilharz 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) From 43e958b5d04eae7df47e91b0097133b1a95b6562 Mon Sep 17 00:00:00 2001 From: Eberhard Beilharz Date: Thu, 16 Jan 2025 16:46:12 +0100 Subject: [PATCH 28/52] chore(linux): update copyright year --- linux/debian/copyright | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/linux/debian/copyright b/linux/debian/copyright index 3956e1463d..46f156e19b 100644 --- a/linux/debian/copyright +++ b/linux/debian/copyright @@ -4,24 +4,24 @@ Upstream-Contact: Keyman team 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 2008-2013, Peng Huang 2008-2021, Red Hat, Inc. 2015-2021, Takao Fujiwara - 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 - 2022-2024 SIL Global + 2022-2025 SIL Global License: MIT License: MIT From 1d342ad0d7007ab41fdde3fe6d0e9bead18bf9b9 Mon Sep 17 00:00:00 2001 From: rc-swag <58423624+rc-swag@users.noreply.github.com> Date: Fri, 17 Jan 2025 08:23:32 +1000 Subject: [PATCH 29/52] chore(windows): remove postinstall state from diagram The postinstall state no longer exists due to the way interaction with the microsoft installer works. This PR just updateds the diagram to match. --- .../src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md b/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md index 160d54b2c1..34383de72e 100644 --- a/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md +++ b/windows/src/desktop/kmshell/main/BackgroundUpdateStateDiagram.md @@ -6,6 +6,5 @@ stateDiagram Downloading --> Installing Downloading --> WaitingRestart WaitingRestart --> Installing - Installing --> PostInstall - PostInstall --> Idle + Installing --> Idle ``` From ea8a9cdbb9e56cf8d7135f9e4651a6895c648110 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Thu, 16 Jan 2025 19:31:47 -0500 Subject: [PATCH 30/52] auto: increment master version to 18.0.170 --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 0e676f9040..78352f95e5 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 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) diff --git a/VERSION.md b/VERSION.md index b7db0fe155..df7920cb67 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.169 \ No newline at end of file +18.0.170 \ No newline at end of file From c0bfb541face0266cf64c07ae156dd121df93c0b Mon Sep 17 00:00:00 2001 From: Darcy Wong Date: Fri, 17 Jan 2025 10:28:32 +0700 Subject: [PATCH 31/52] chore(common): Add 17.0.333 to version history --- HISTORY.md | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 78352f95e5..7f830394da 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -953,7 +953,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) @@ -1149,6 +1149,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) From 7cb6be6a97d06142085437904736367fa5d8dc86 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Fri, 17 Jan 2025 11:03:20 +0700 Subject: [PATCH 32/52] fix(developer): support `&displayMap` font in web debugger Adds a new design-time font property for when `&displayMap` is in use in a keyboard, so that the web debugger can use that font as appropriate. If the keyboard does not have a `&displayMap` store, then the DisplayMap font property is disabled, and the web debugger will use the OSK Font instead, as previously was the case. Note that the font for `&displayMap` has to be installed on the keyboard author's system in order for it to be served up. Fixes: #12123 --- .../delphi/keyboards/kmxfileconsts.pas | 6 +- .../common/delphi/general/KeyboardFonts.pas | 5 +- developer/src/tike/child/UfrmKeymanWizard.pas | 32 +++++++++++ .../src/tike/dialogs/UfrmKeyboardFonts.dfm | 56 +++++++++++++++++-- .../src/tike/dialogs/UfrmKeyboardFonts.pas | 8 +++ ....Developer.UI.Project.kmnProjectFileUI.pas | 15 +++-- 6 files changed, 107 insertions(+), 15 deletions(-) diff --git a/common/windows/delphi/keyboards/kmxfileconsts.pas b/common/windows/delphi/keyboards/kmxfileconsts.pas index e8f0e0d740..8e1955aaa6 100644 --- a/common/windows/delphi/keyboards/kmxfileconsts.pas +++ b/common/windows/delphi/keyboards/kmxfileconsts.pas @@ -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 diff --git a/developer/src/common/delphi/general/KeyboardFonts.pas b/developer/src/common/delphi/general/KeyboardFonts.pas index 5c80392521..f1a2bac2b9 100644 --- a/developer/src/common/delphi/general/KeyboardFonts.pas +++ b/developer/src/common/delphi/general/KeyboardFonts.pas @@ -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 diff --git a/developer/src/tike/child/UfrmKeymanWizard.pas b/developer/src/tike/child/UfrmKeymanWizard.pas index 59e0f8a341..afb1e715ed 100644 --- a/developer/src/tike/child/UfrmKeymanWizard.pas +++ b/developer/src/tike/child/UfrmKeymanWizard.pas @@ -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 diff --git a/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm b/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm index bd8ba4de5b..b874db58a8 100644 --- a/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm +++ b/developer/src/tike/dialogs/UfrmKeyboardFonts.dfm @@ -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 diff --git a/developer/src/tike/dialogs/UfrmKeyboardFonts.pas b/developer/src/tike/dialogs/UfrmKeyboardFonts.pas index 8c5bc103c2..7f5d4546bd 100644 --- a/developer/src/tike/dialogs/UfrmKeyboardFonts.pas +++ b/developer/src/tike/dialogs/UfrmKeyboardFonts.pas @@ -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; diff --git a/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas b/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas index 863b737dc0..10e9dcdd3c 100644 --- a/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas +++ b/developer/src/tike/project/Keyman.Developer.UI.Project.kmnProjectFileUI.pas @@ -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 From b4ca7cb8c69bcf5d4ec66b6406b9ca2d963d1ffc Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Fri, 17 Jan 2025 15:29:47 +0700 Subject: [PATCH 33/52] fix(web): patches up unit test with execution dependent on autocorrect state --- .../interfaces/prediction/predictionContext.tests.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.js b/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.js index 4217aeb69b..9aa9cd8015 100644 --- a/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.js +++ b/web/src/test/auto/headless/engine/interfaces/prediction/predictionContext.tests.js @@ -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() { From 1ed44014d96a7b9c3bf8354ff46d6000194aef7f Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Fri, 17 Jan 2025 13:01:57 -0500 Subject: [PATCH 34/52] auto: increment master version to 18.0.171 --- HISTORY.md | 7 +++++++ VERSION.md | 2 +- 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 78352f95e5..2940491b3a 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,12 @@ # Keyman Version History +## 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) diff --git a/VERSION.md b/VERSION.md index df7920cb67..d4aee009e5 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.170 \ No newline at end of file +18.0.171 \ No newline at end of file From 548866e1a519539b7fd5299ebd6d0b86dc3759fe Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Sat, 18 Jan 2025 13:01:28 -0500 Subject: [PATCH 35/52] auto: increment master version to 18.0.172 --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 2940491b3a..6d02a97e26 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # Keyman Version History +## 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) diff --git a/VERSION.md b/VERSION.md index d4aee009e5..02da667937 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.171 \ No newline at end of file +18.0.172 \ No newline at end of file From 5c1a15591adf28dfb3ec2b94195e20460a4fe575 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Sun, 19 Jan 2025 13:01:08 -0500 Subject: [PATCH 36/52] auto: increment master version to 18.0.173 --- HISTORY.md | 4 ++++ VERSION.md | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 71c44403a8..be484aee8b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,9 @@ # Keyman Version History +## 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) diff --git a/VERSION.md b/VERSION.md index 02da667937..5f77e86146 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.172 \ No newline at end of file +18.0.173 \ No newline at end of file From 45a6291a3e34c43b51bd0563bdac8233c74200f9 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 20 Jan 2025 09:19:47 +0700 Subject: [PATCH 37/52] refactor(developer): move ttfInfo to developer-utils We will need ttfInfo in kmc-package, so move it to the shared module. --- .../web/utils}/src/font-family.ts | 27 +++++++++++++++++++ developer/src/common/web/utils/src/index.ts | 4 ++- .../web/utils}/src/ttfmeta/LICENSE | 0 .../web/utils}/src/ttfmeta/README.md | 0 .../web/utils}/src/ttfmeta/lib/index.js | 16 ++++++++++- .../web/utils}/src/ttfmeta/lib/main.js | 10 +++++++ .../web/utils}/src/ttfmeta/lib/meta.js | 0 .../web/utils}/src/ttfmeta/test/local.mjs | 0 .../utils}/src/ttfmeta/test/mocha-index.mjs | 0 developer/src/common/web/utils/tsconfig.json | 4 +++ .../src/keyboard-info-compiler.ts | 2 +- .../test/font-family.tests.ts | 2 +- developer/src/kmc-keyboard-info/tsconfig.json | 1 - 13 files changed, 61 insertions(+), 5 deletions(-) rename developer/src/{kmc-keyboard-info => common/web/utils}/src/font-family.ts (51%) rename developer/src/{kmc-keyboard-info => common/web/utils}/src/ttfmeta/LICENSE (100%) rename developer/src/{kmc-keyboard-info => common/web/utils}/src/ttfmeta/README.md (100%) rename developer/src/{kmc-keyboard-info => common/web/utils}/src/ttfmeta/lib/index.js (73%) rename developer/src/{kmc-keyboard-info => common/web/utils}/src/ttfmeta/lib/main.js (97%) rename developer/src/{kmc-keyboard-info => common/web/utils}/src/ttfmeta/lib/meta.js (100%) rename developer/src/{kmc-keyboard-info => common/web/utils}/src/ttfmeta/test/local.mjs (100%) rename developer/src/{kmc-keyboard-info => common/web/utils}/src/ttfmeta/test/mocha-index.mjs (100%) diff --git a/developer/src/kmc-keyboard-info/src/font-family.ts b/developer/src/common/web/utils/src/font-family.ts similarity index 51% rename from developer/src/kmc-keyboard-info/src/font-family.ts rename to developer/src/common/web/utils/src/font-family.ts index a78e341de7..d8fad53e37 100644 --- a/developer/src/kmc-keyboard-info/src/font-family.ts +++ b/developer/src/common/web/utils/src/font-family.ts @@ -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; } \ No newline at end of file diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 290efd5fc7..19845f0e8f 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -68,4 +68,6 @@ 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'; \ No newline at end of file +export * as CloudUrls from './cloud-urls.js'; + +export { getFontFamily, getFontFamilySync } from './font-family.js'; \ No newline at end of file diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/LICENSE b/developer/src/common/web/utils/src/ttfmeta/LICENSE similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/LICENSE rename to developer/src/common/web/utils/src/ttfmeta/LICENSE diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/README.md b/developer/src/common/web/utils/src/ttfmeta/README.md similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/README.md rename to developer/src/common/web/utils/src/ttfmeta/README.md diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js b/developer/src/common/web/utils/src/ttfmeta/lib/index.js similarity index 73% rename from developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js rename to developer/src/common/web/utils/src/ttfmeta/lib/index.js index eb94d53c1f..53972297f1 100644 --- a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/index.js +++ b/developer/src/common/web/utils/src/ttfmeta/lib/index.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} @@ -48,5 +62,5 @@ export function promise(pathOrData) { } /** @namespace */ -export const ttfMeta = { ttfInfo, promise }; +export const ttfMeta = { ttfInfo, ttfInfoSync, promise }; export default ttfMeta; diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js b/developer/src/common/web/utils/src/ttfmeta/lib/main.js similarity index 97% rename from developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js rename to developer/src/common/web/utils/src/ttfmeta/lib/main.js index 239a6244dc..a952512f26 100644 --- a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/main.js +++ b/developer/src/common/web/utils/src/ttfmeta/lib/main.js @@ -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} diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/lib/meta.js b/developer/src/common/web/utils/src/ttfmeta/lib/meta.js similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/lib/meta.js rename to developer/src/common/web/utils/src/ttfmeta/lib/meta.js diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/test/local.mjs b/developer/src/common/web/utils/src/ttfmeta/test/local.mjs similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/test/local.mjs rename to developer/src/common/web/utils/src/ttfmeta/test/local.mjs diff --git a/developer/src/kmc-keyboard-info/src/ttfmeta/test/mocha-index.mjs b/developer/src/common/web/utils/src/ttfmeta/test/mocha-index.mjs similarity index 100% rename from developer/src/kmc-keyboard-info/src/ttfmeta/test/mocha-index.mjs rename to developer/src/common/web/utils/src/ttfmeta/test/mocha-index.mjs diff --git a/developer/src/common/web/utils/tsconfig.json b/developer/src/common/web/utils/tsconfig.json index 6f96f75ee7..362ec3a972 100644 --- a/developer/src/common/web/utils/tsconfig.json +++ b/developer/src/common/web/utils/tsconfig.json @@ -5,8 +5,12 @@ "rootDir": ".", "outDir": "./build/", "baseUrl": ".", + + /** for ttfmeta */ + "allowJs": true, }, "include": [ "src/**/*.ts", + "src/ttfmeta/lib/*.js", ], } diff --git a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts index ee784429b9..3e1db45aed 100644 --- a/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts +++ b/developer/src/kmc-keyboard-info/src/keyboard-info-compiler.ts @@ -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" }); diff --git a/developer/src/kmc-keyboard-info/test/font-family.tests.ts b/developer/src/kmc-keyboard-info/test/font-family.tests.ts index b27fad8d74..d926e75c65 100644 --- a/developer/src/kmc-keyboard-info/test/font-family.tests.ts +++ b/developer/src/kmc-keyboard-info/test/font-family.tests.ts @@ -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"); diff --git a/developer/src/kmc-keyboard-info/tsconfig.json b/developer/src/kmc-keyboard-info/tsconfig.json index d09b25c560..124c812ecf 100644 --- a/developer/src/kmc-keyboard-info/tsconfig.json +++ b/developer/src/kmc-keyboard-info/tsconfig.json @@ -13,7 +13,6 @@ }, "include": [ "src/**/*.ts", - "src/ttfmeta/lib/*.js", "src/imports/langtags.js", ], "references": [ From 0b749a5df2b9b3b7aca1d493cf93017ee0ca8af5 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 20 Jan 2025 09:21:50 +0700 Subject: [PATCH 38/52] feat(developer): rewrite font data in .kvk from package metadata When compiling a package, rewrite the font data in embedded .kvk files to use the facename from the font referenced in OSKFont or DisplayFont. This also does some safety checking for cross-references of the .kvk, and the font data. Fixes: #12912 --- .../kmc-package/src/compiler/kmp-compiler.ts | 113 ++++++++++++++---- .../src/compiler/kvk-font-facename.ts | 16 +++ .../src/compiler/package-compiler-messages.ts | 20 ++++ 3 files changed, 124 insertions(+), 25 deletions(-) create mode 100644 developer/src/kmc-package/src/compiler/kvk-font-facename.ts diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index 607823c7dd..ca15cae14d 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -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,35 @@ 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)) { + 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 +558,88 @@ 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 */ - 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; } } diff --git a/developer/src/kmc-package/src/compiler/kvk-font-facename.ts b/developer/src/kmc-package/src/compiler/kvk-font-facename.ts new file mode 100644 index 0000000000..b7c69da8e8 --- /dev/null +++ b/developer/src/kmc-package/src/compiler/kvk-font-facename.ts @@ -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); +} \ No newline at end of file diff --git a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts index 9a593a3a11..6545b7d963 100644 --- a/developer/src/kmc-package/src/compiler/package-compiler-messages.ts +++ b/developer/src/kmc-package/src/compiler/package-compiler-messages.ts @@ -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.` + ); } From d24f3477bd6fab8021b0ae613c32456cf43e0b4f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 20 Jan 2025 10:06:25 +0700 Subject: [PATCH 39/52] chore(developer): update binary_kvk_file test to make .kps more valid The test .kps for binary_kvk_file tests was not very coherent, which meant that it failed with the changes and additional consistency checking included in this PR. --- .../binary_kvk_file/source/binary_kvk_file.kmx | Bin 0 -> 352 bytes .../binary_kvk_file/source/binary_kvk_file.kps | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx diff --git a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kmx new file mode 100644 index 0000000000000000000000000000000000000000..bd6b832cab772d03a45460e6c982d79772a9ab6e GIT binary patch literal 352 zcmZvYyAAdPzftU-(5_7TAk3e<2#yh{6J%lpJ>i; zp8cfbGTcz0i3ZwI$I5E289asBOpaP`wxny)?MPoMtp7qQ?)E9)e`0JAA>To*E2jVe literal 0 HcmV?d00001 diff --git a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps index de17212a84..1a02984beb 100644 --- a/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps +++ b/developer/src/kmc-package/test/fixtures/binary_kvk_file/source/binary_kvk_file.kps @@ -27,7 +27,7 @@ .kvk - ../../invalid/basic.kmx + binary_kvk_file.kmx Keyboard Basic 0 .kmx @@ -36,7 +36,7 @@ Basic - basic + binary_kvk_file 1.0 Khmer From 11d92be941125981e79477b4fdc85b2f6af4f5b8 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 20 Jan 2025 10:25:56 +0700 Subject: [PATCH 40/52] chore: address review comments --- developer/src/kmc-package/src/compiler/kmp-compiler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/developer/src/kmc-package/src/compiler/kmp-compiler.ts b/developer/src/kmc-package/src/compiler/kmp-compiler.ts index ca15cae14d..cd6cf6df36 100644 --- a/developer/src/kmc-package/src/compiler/kmp-compiler.ts +++ b/developer/src/kmc-package/src/compiler/kmp-compiler.ts @@ -495,6 +495,7 @@ export class KmpCompiler implements KeymanCompiler { 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); @@ -624,8 +625,7 @@ export class KmpCompiler implements KeymanCompiler { /** * 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 isKvkFileBinary(data: Uint8Array) { if(data.byteLength < 4) { From f407d9823d534d496ce8ddafd7bf8da237bd9192 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 20 Jan 2025 13:06:01 +0700 Subject: [PATCH 41/52] fix(developer): include developer-utils deps in server build Fixes: #12950 --- developer/src/server/build.sh | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh index c73ed853f7..65f38dd8df 100755 --- a/developer/src/server/build.sh +++ b/developer/src/server/build.sh @@ -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 From 357d1961819bf8d725f484e39cefe744d776d71e Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Mon, 20 Jan 2025 14:11:42 +0700 Subject: [PATCH 42/52] chore(developer): use JQ variable in script --- developer/src/server/build.sh | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/developer/src/server/build.sh b/developer/src/server/build.sh index 65f38dd8df..e0e82e3617 100755 --- a/developer/src/server/build.sh +++ b/developer/src/server/build.sh @@ -117,7 +117,7 @@ function installer_server() { # #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" \ + "$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")' \ From 9eb151c781c5831bc6dd7f3ce089c74380fa2571 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Mon, 20 Jan 2025 13:02:42 -0500 Subject: [PATCH 43/52] auto: increment master version to 18.0.174 --- HISTORY.md | 6 ++++++ VERSION.md | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index be484aee8b..62ad9f552c 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,11 @@ # Keyman Version History +## 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) diff --git a/VERSION.md b/VERSION.md index 5f77e86146..736c025123 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.173 \ No newline at end of file +18.0.174 \ No newline at end of file From bda60eb39f9c5560b3562d79fe68109a5529d72f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 21 Jan 2025 10:55:18 +0700 Subject: [PATCH 44/52] chore(developer): validate targets parameter in kmc-generate Fixes: #12954 --- .../src/kmc-generate/src/basic-generator.ts | 19 +++++++++- .../kmc-generate/src/generator-messages.ts | 37 ++++++++++++++----- .../src/keyman-keyboard-generator.ts | 5 ++- .../src/ldml-keyboard-generator.ts | 5 ++- .../src/lexical-model-generator.ts | 5 ++- .../src/kmc-generate/test/messages.tests.ts | 14 +++++++ 6 files changed, 71 insertions(+), 14 deletions(-) diff --git a/developer/src/kmc-generate/src/basic-generator.ts b/developer/src/kmc-generate/src/basic-generator.ts index cb69ef5fdd..fe5a5cd304 100644 --- a/developer/src/kmc-generate/src/basic-generator.ts +++ b/developer/src/kmc-generate/src/basic-generator.ts @@ -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`); diff --git a/developer/src/kmc-generate/src/generator-messages.ts b/developer/src/kmc-generate/src/generator-messages.ts index cda0d82fd2..82e1a4bfa6 100644 --- a/developer/src/kmc-generate/src/generator-messages.ts +++ b/developer/src/kmc-generate/src/generator-messages.ts @@ -17,30 +17,47 @@ 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 `+ - `..`); + `..` + ); + + 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.` + ); }; diff --git a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts index c3d5c7665a..27d0adb99f 100644 --- a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts +++ b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts @@ -39,7 +39,10 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom * @returns Binary artifacts on success, null on failure. */ async run(): Promise { - this.preGenerate(); + if(!this.preGenerate()) { + // errors will have been reported in preGenerate + return null; + } const artifacts: GeneratorArtifacts = this.defaultArtifacts(); diff --git a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts index e9885e7fb3..3be00cb329 100644 --- a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts +++ b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts @@ -28,7 +28,10 @@ export class LdmlKeyboardGenerator extends BasicGenerator implements KeymanCompi * @returns Binary artifacts on success, null on failure. */ async run(): Promise { - this.preGenerate(); + if(!this.preGenerate()) { + // errors will have been reported in preGenerate + return null; + } const artifacts: GeneratorArtifacts = this.defaultArtifacts(); this.templatePath = 'ldml-keyboard'; diff --git a/developer/src/kmc-generate/src/lexical-model-generator.ts b/developer/src/kmc-generate/src/lexical-model-generator.ts index f78807798c..f0cff6558b 100644 --- a/developer/src/kmc-generate/src/lexical-model-generator.ts +++ b/developer/src/kmc-generate/src/lexical-model-generator.ts @@ -31,7 +31,10 @@ export class LexicalModelGenerator extends BasicGenerator implements KeymanCompi * @returns Binary artifacts on success, null on failure. */ async run(): Promise { - this.preGenerate(); + if(!this.preGenerate()) { + // errors will have been reported in preGenerate + return null; + } const artifacts: GeneratorArtifacts = this.defaultArtifacts(); diff --git a/developer/src/kmc-generate/test/messages.tests.ts b/developer/src/kmc-generate/test/messages.tests.ts index 68c7f741fa..eebb17cda5 100644 --- a/developer/src/kmc-generate/test/messages.tests.ts +++ b/developer/src/kmc-generate/test/messages.tests.ts @@ -11,6 +11,8 @@ import { GeneratorMessages } from '../src/generator-messages.js'; import { TestCompilerCallbacks, verifyCompilerMessagesObject } from '@keymanapp/developer-test-helpers'; import { CompilerErrorNamespace } from '@keymanapp/developer-utils'; import { AbstractGenerator, GeneratorOptions } from '../src/abstract-generator.js'; +import { BasicGenerator } from '../src/basic-generator.js'; +import { options } from './shared-options.js'; describe('GeneratorMessages', function () { const callbacks = new TestCompilerCallbacks(); @@ -62,4 +64,16 @@ 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)); + }); }); From 3ad0dec493e57f86461c0eb9f5be6a24d9c8fda5 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 21 Jan 2025 12:41:52 +0700 Subject: [PATCH 45/52] chore(developer): validate keyboard and model IDs in kmc-generate Fixes: #12957 --- developer/src/common/web/utils/src/index.ts | 4 +- .../src/common/web/utils/src/valid-ids.ts | 38 ++++++++++ .../common/web/utils/test/valid-ids.tests.ts | 76 +++++++++++++++++++ developer/src/kmc-generate/package.json | 3 +- .../kmc-generate/src/generator-messages.ts | 18 +++++ .../src/keyman-keyboard-generator.ts | 8 +- .../src/ldml-keyboard-generator.ts | 8 +- .../src/lexical-model-generator.ts | 7 +- ...s.tests.ts => generator-messages.tests.ts} | 46 ++++++++++- 9 files changed, 200 insertions(+), 8 deletions(-) create mode 100644 developer/src/common/web/utils/src/valid-ids.ts create mode 100644 developer/src/common/web/utils/test/valid-ids.tests.ts rename developer/src/kmc-generate/test/{messages.tests.ts => generator-messages.tests.ts} (64%) diff --git a/developer/src/common/web/utils/src/index.ts b/developer/src/common/web/utils/src/index.ts index 19845f0e8f..f8ee6246da 100644 --- a/developer/src/common/web/utils/src/index.ts +++ b/developer/src/common/web/utils/src/index.ts @@ -70,4 +70,6 @@ export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader } from './xml-utils.js' export * as GitHubUrls from './github-urls.js'; export * as CloudUrls from './cloud-urls.js'; -export { getFontFamily, getFontFamilySync } from './font-family.js'; \ No newline at end of file +export { getFontFamily, getFontFamilySync } from './font-family.js'; + +export * as ValidIds from './valid-ids.js'; diff --git a/developer/src/common/web/utils/src/valid-ids.ts b/developer/src/common/web/utils/src/valid-ids.ts new file mode 100644 index 0000000000..717a173f5d --- /dev/null +++ b/developer/src/common/web/utils/src/valid-ids.ts @@ -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); +} diff --git a/developer/src/common/web/utils/test/valid-ids.tests.ts b/developer/src/common/web/utils/test/valid-ids.tests.ts new file mode 100644 index 0000000000..c285332b66 --- /dev/null +++ b/developer/src/common/web/utils/test/valid-ids.tests.ts @@ -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`)); + }); +}); diff --git a/developer/src/kmc-generate/package.json b/developer/src/kmc-generate/package.json index a8e0810338..ba488ff575 100644 --- a/developer/src/kmc-generate/package.json +++ b/developer/src/kmc-generate/package.json @@ -54,7 +54,8 @@ "exclude-after-remap": true, "exclude": [ "test/", - "src/template/" + "src/template/", + "src/main.ts" ] }, "repository": { diff --git a/developer/src/kmc-generate/src/generator-messages.ts b/developer/src/kmc-generate/src/generator-messages.ts index 82e1a4bfa6..ad562dbd29 100644 --- a/developer/src/kmc-generate/src/generator-messages.ts +++ b/developer/src/kmc-generate/src/generator-messages.ts @@ -60,4 +60,22 @@ export class GeneratorMessages { 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'.` + ); }; diff --git a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts index 27d0adb99f..ec76beff65 100644 --- a/developer/src/kmc-generate/src/keyman-keyboard-generator.ts +++ b/developer/src/kmc-generate/src/keyman-keyboard-generator.ts @@ -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,6 +40,11 @@ export class KeymanKeyboardGenerator extends BasicGenerator implements KeymanCom * @returns Binary artifacts on success, null on failure. */ async run(): Promise { + 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; diff --git a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts index 3be00cb329..f8afb49013 100644 --- a/developer/src/kmc-generate/src/ldml-keyboard-generator.ts +++ b/developer/src/kmc-generate/src/ldml-keyboard-generator.ts @@ -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,6 +29,11 @@ export class LdmlKeyboardGenerator extends BasicGenerator implements KeymanCompi * @returns Binary artifacts on success, null on failure. */ async run(): Promise { + 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; diff --git a/developer/src/kmc-generate/src/lexical-model-generator.ts b/developer/src/kmc-generate/src/lexical-model-generator.ts index f0cff6558b..86316549cf 100644 --- a/developer/src/kmc-generate/src/lexical-model-generator.ts +++ b/developer/src/kmc-generate/src/lexical-model-generator.ts @@ -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,6 +31,11 @@ export class LexicalModelGenerator extends BasicGenerator implements KeymanCompi * @returns Binary artifacts on success, null on failure. */ async run(): Promise { + 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; diff --git a/developer/src/kmc-generate/test/messages.tests.ts b/developer/src/kmc-generate/test/generator-messages.tests.ts similarity index 64% rename from developer/src/kmc-generate/test/messages.tests.ts rename to developer/src/kmc-generate/test/generator-messages.tests.ts index eebb17cda5..b5d3a8837a 100644 --- a/developer/src/kmc-generate/test/messages.tests.ts +++ b/developer/src/kmc-generate/test/generator-messages.tests.ts @@ -2,16 +2,23 @@ * 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 () { @@ -76,4 +83,37 @@ describe('GeneratorMessages', function () { 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)); + }); }); From 2dec427bd438392fa9293e4e7617be54d54b9dff Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 21 Jan 2025 13:55:48 +0700 Subject: [PATCH 46/52] fix(developer): use correct terms in kmc-generate lexical model help Fixes: #12959 --- developer/src/kmc/src/commands/generate.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/developer/src/kmc/src/commands/generate.ts b/developer/src/kmc/src/commands/generate.ts index bac724d9d4..54c0cffce6 100644 --- a/developer/src/kmc/src/commands/generate.ts +++ b/developer/src/kmc/src/commands/generate.ts @@ -59,12 +59,12 @@ function declareGenerateLexicalModel(command: Command) { modelSubCommand .description('Generate a wordlist lexical model project') .option('-o, --out-path ', 'Output path (may exist)') - .option('-n, --name ', 'Keyboard descriptive name') + .option('-n, --name ', 'Lexical model descriptive name') .option('-c, --copyright ', 'Copyright holder') /* © yyyy */ - .option('-v, --version ', 'Keyboard version', '1.0') + .option('-v, --version ', 'Lexical model version', '1.0') .option('-L, --language-tag ', 'BCP-47 language tag', (value, previous) => previous.concat([value]), []) - .option('-a, --author ', 'Name of keyboard author') + .option('-a, --author ', 'Name of lexical model author') .option('-d, --description ', 'Short description of the project, Markdown') .action(generateLexicalModel); From 6f6316e79bd59a47b2fa7c74c409f3065a06304f Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 21 Jan 2025 13:58:37 +0700 Subject: [PATCH 47/52] fix(developer): use 1.0.0 as default for ldml keyboards in kmc-generate Fixes: #12961 --- developer/src/kmc/src/commands/generate.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/developer/src/kmc/src/commands/generate.ts b/developer/src/kmc/src/commands/generate.ts index 54c0cffce6..c9d8dde391 100644 --- a/developer/src/kmc/src/commands/generate.ts +++ b/developer/src/kmc/src/commands/generate.ts @@ -45,7 +45,7 @@ function declareGenerateLdmlKeyboard(command: Command) { .option('-o, --out-path ', 'Output path (may exist)') .option('-n, --name ', 'Keyboard descriptive name') .option('-c, --copyright ', 'Copyright holder') /* © yyyy */ - .option('-v, --version ', 'Keyboard version', '1.0') + .option('-v, --version ', 'Keyboard version', '1.0.0') .option('-L, --language-tag ', 'BCP-47 language tag', (value, previous) => previous.concat([value]), []) .option('-a, --author ', 'Name of keyboard author') From 44483be68898ab73a448c941fa05c7e4d6ba9110 Mon Sep 17 00:00:00 2001 From: Joshua Horton Date: Tue, 21 Jan 2025 14:32:16 +0700 Subject: [PATCH 48/52] fix(web): prevents multitap warning that occurs multitapping longpressable modifier keys Fixes: #12963 --- .../src/input/gestures/browser/multitap.ts | 32 +++++++++++++------ 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/web/src/engine/osk/src/input/gestures/browser/multitap.ts b/web/src/engine/osk/src/input/gestures/browser/multitap.ts index 99e50810f9..76280f63e7 100644 --- a/web/src/engine/osk/src/input/gestures/browser/multitap.ts +++ b/web/src/engine/osk/src/input/gestures/browser/multitap.ts @@ -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. From 8459472f75fbef34f4ee00e5603cda3ba9df6b94 Mon Sep 17 00:00:00 2001 From: Meng-Heng Date: Tue, 21 Jan 2025 15:57:54 +0700 Subject: [PATCH 49/52] chore: adds h1 tags --- web/docs/engine/guide/examples/__auto-control.html | 3 +++ web/docs/engine/guide/examples/__control-by-control.html | 2 ++ web/docs/engine/guide/examples/__first-example.html | 3 +++ web/docs/engine/guide/examples/__full-manual-control.html | 3 ++- web/docs/engine/guide/examples/__manual-control.html | 3 ++- 5 files changed, 12 insertions(+), 2 deletions(-) diff --git a/web/docs/engine/guide/examples/__auto-control.html b/web/docs/engine/guide/examples/__auto-control.html index 9e1786c6d7..e7fc2768c6 100644 --- a/web/docs/engine/guide/examples/__auto-control.html +++ b/web/docs/engine/guide/examples/__auto-control.html @@ -20,9 +20,12 @@ +

Automatic Mode Example

+ +Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__control-by-control.html b/web/docs/engine/guide/examples/__control-by-control.html index b1f7ac5e42..440a7862d0 100644 --- a/web/docs/engine/guide/examples/__control-by-control.html +++ b/web/docs/engine/guide/examples/__control-by-control.html @@ -33,6 +33,7 @@ +

Control-by-Control Example

Email to (KeymanWeb not enabled)

@@ -43,5 +44,6 @@
+Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__first-example.html b/web/docs/engine/guide/examples/__first-example.html index 3b46160f65..1657db9440 100644 --- a/web/docs/engine/guide/examples/__first-example.html +++ b/web/docs/engine/guide/examples/__first-example.html @@ -18,6 +18,9 @@ +

First Example

Click me and type:

+ +Back to Document \ No newline at end of file diff --git a/web/docs/engine/guide/examples/__full-manual-control.html b/web/docs/engine/guide/examples/__full-manual-control.html index c9d76d90fb..24b0d004a7 100644 --- a/web/docs/engine/guide/examples/__full-manual-control.html +++ b/web/docs/engine/guide/examples/__full-manual-control.html @@ -43,7 +43,7 @@ window.addEventListener( 'load' , SetupDocument ); - +

Manual Control - Custom Interface

Keyboard:

@@ -51,5 +51,6 @@ window.addEventListener( 'load' , SetupDocument );

+Back to Document diff --git a/web/docs/engine/guide/examples/__manual-control.html b/web/docs/engine/guide/examples/__manual-control.html index 37776a7850..e9f5f219ca 100644 --- a/web/docs/engine/guide/examples/__manual-control.html +++ b/web/docs/engine/guide/examples/__manual-control.html @@ -32,7 +32,7 @@ - +

Manual Mode Example

KeymanWeb

@@ -40,6 +40,7 @@

+Back to Document
\ No newline at end of file From e86ddc89a283f07f4dd45e2b8d6427b2dc31090e Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Tue, 21 Jan 2025 15:58:19 +0700 Subject: [PATCH 50/52] chore(developer): add test for basic transform to kmc-generate Fixes: #12965 --- .../test/basic-generator.tests.ts | 56 +++++++++++++++++-- .../test/fixtures/basic/welcome.htm | 26 +++++++++ 2 files changed, 78 insertions(+), 4 deletions(-) create mode 100644 developer/src/kmc-generate/test/fixtures/basic/welcome.htm diff --git a/developer/src/kmc-generate/test/basic-generator.tests.ts b/developer/src/kmc-generate/test/basic-generator.tests.ts index b1eb1e420d..41b4e0d99f 100644 --- a/developer/src/kmc-generate/test/basic-generator.tests.ts +++ b/developer/src/kmc-generate/test/basic-generator.tests.ts @@ -2,22 +2,70 @@ * Keyman is copyright (C) SIL International. MIT License. */ +import * as fs from 'node:fs'; + 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('/', '\\')) + ); + + assert.hasAllKeys(artifacts, keys); + + // pick one file and compare it + const filename = BasicGeneratorTest.files()[0].replaceAll('/', '\\'); // 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); + }); }); diff --git a/developer/src/kmc-generate/test/fixtures/basic/welcome.htm b/developer/src/kmc-generate/test/fixtures/basic/welcome.htm new file mode 100644 index 0000000000..1454a604b6 --- /dev/null +++ b/developer/src/kmc-generate/test/fixtures/basic/welcome.htm @@ -0,0 +1,26 @@ + + + + + + Start Using Sample Project + + + + +

Start Using Sample Project

+ +

+ # A mighty description +

+ +

Keyboard Layout

+ + + + + \ No newline at end of file From 071a8fbde4c4d446ecb30c1621313213adea4796 Mon Sep 17 00:00:00 2001 From: Keyman Build Agent Date: Tue, 21 Jan 2025 13:01:35 -0500 Subject: [PATCH 51/52] auto: increment master version to 18.0.175 --- HISTORY.md | 5 +++++ VERSION.md | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/HISTORY.md b/HISTORY.md index 62ad9f552c..6ffda9827b 100644 --- a/HISTORY.md +++ b/HISTORY.md @@ -1,5 +1,10 @@ # 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) diff --git a/VERSION.md b/VERSION.md index 736c025123..e1b3341dda 100644 --- a/VERSION.md +++ b/VERSION.md @@ -1 +1 @@ -18.0.174 \ No newline at end of file +18.0.175 \ No newline at end of file From 89813e11adfb777dafa69beb08a9c31bf5f0f356 Mon Sep 17 00:00:00 2001 From: Marc Durdin Date: Wed, 22 Jan 2025 07:45:41 +0700 Subject: [PATCH 52/52] chore(developer): use path separator for non-Windows platforms in kmc-generate tests --- developer/src/kmc-generate/test/basic-generator.tests.ts | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/developer/src/kmc-generate/test/basic-generator.tests.ts b/developer/src/kmc-generate/test/basic-generator.tests.ts index 41b4e0d99f..f5cff4044a 100644 --- a/developer/src/kmc-generate/test/basic-generator.tests.ts +++ b/developer/src/kmc-generate/test/basic-generator.tests.ts @@ -3,6 +3,7 @@ */ import * as fs from 'node:fs'; +import * as path from 'node:path'; import 'mocha'; import { assert } from 'chai'; @@ -54,13 +55,13 @@ describe('BasicGenerator', function () { // standard artifacts are listed in AbstractGenerator const keys = [].concat( 'kmc-generate:outputPath', - BasicGeneratorTest.files().map(f => f.replaceAll('/', '\\')) + BasicGeneratorTest.files().map(f => f.replaceAll('/', path.sep)) ); assert.hasAllKeys(artifacts, keys); // pick one file and compare it - const filename = BasicGeneratorTest.files()[0].replaceAll('/', '\\'); // welcome.htm + 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');