chore(web): Merge branch 'master' into maint/web/13399_tc-config_refactor

This commit is contained in:
Eberhard Beilharz 2025-06-04 18:20:39 +02:00
commit ab2f7c2d30
No known key found for this signature in database
GPG key ID: E9140597606020D3
84 changed files with 1272 additions and 647 deletions

View file

@ -31,7 +31,7 @@ runs:
path: artifacts/keyman-srcpkg
- name: Build
uses: sillsdev/gha-ubuntu-packaging@b619077451b27c16dc6fd699bc1daf8d5ce07659 # v1.2
uses: sillsdev/gha-ubuntu-packaging@556b268762be9bea0f39861a7392587211ef6a58 # v2.0.1
with:
dist: "${{ inputs.dist }}"
platform: "${{ inputs.arch }}"

47
.github/labeler.yml vendored
View file

@ -11,43 +11,56 @@ docs: docs/**
# Add labels based on changed files using actions/labeler
#
android/: android/**
android/:
- android/**
- resources/teamcity/includes/**
- resources/teamcity/android/**
android/app/: android/KMAPro/**
android/engine/: android/KMEA/**
android/samples/: android/Samples/**
common/: common/**
common/:
- common/**
- resources/teamcity/includes/**
- resources/teamcity/common/**
common/web/: common/web/**
core/:
- core/**
- resources/teamcity/includes/**
- resources/teamcity/core/**
developer/:
- developer/**
- resources/teamcity/includes/**
- resources/teamcity/developer/**
developer/compilers/:
- developer/src/kmc/**
- developer/src/kmcmplib/**
- developer/src/kmc-*/**
developer/ide/:
- developer/src/server/**
- developer/src/tike/**
ios/: ios/**
ios/:
- ios/**
- resources/teamcity/includes/**
- resources/teamcity/ios/**
ios/app/: ios/keyman/**
ios/engine/: ios/engine/**
ios/samples/: ios/samples/**
linux/: linux/**
linux/:
- linux/**
- resources/teamcity/includes/**
- resources/teamcity/linux/**
linux/config/: linux/keyman-config/**
linux/engine/:
- linux/ibus-keyman/**
- linux/legacy/ibus-kmfl/**
- linux/legacy/kmflcomp/**
- linux/legacy/libkmfl/**
linux/engine/: linux/ibus-keyman/**
mac/: mac/**
mac/:
- mac/**
- resources/teamcity/includes/**
- resources/teamcity/mac/**
# mac/config/:
# mac/engine/: mac/**
@ -59,13 +72,19 @@ oem/fv/windows/: oem/firstvoices/windows/**
resources/: resources/**
web/: web/**
web/:
- web/**
- resources/teamcity/includes/**
- resources/teamcity/web/**
# web/bookmarklet/
web/engine/: web/source/**
web/ui/: web/source/kmwui*
web/samples/: web/samples/**
web/predictive-text/: web/src/engine/predictive-text/**
windows/: windows/**
windows/:
- windows/**
- resources/teamcity/includes/**
- resources/teamcity/windows/**
windows/config/: windows/src/desktop/**
windows/engine/: windows/src/engine/**

View file

@ -1,5 +1,48 @@
# Keyman Version History
## 19.0.57 alpha 2025-06-03
* fix(windows): change from error to breadcrumb and log (#14069)
* maint(mac): make make-km-dmg.sh less quiet (#14105)
## 19.0.56 alpha 2025-06-02
* maint(ios): Use zip.inc.sh to prep release artifacts (#13919)
* maint(linux): update to new version of gha-ubuntu-packaging (#14094)
* chore(linux): add Python TC test runner (#14062)
## 19.0.55 alpha 2025-05-31
* maint(linux): `sudo` not required for removing temporary files (#14086)
* maint(linux): update to new version of gha-ubuntu-packaging (#14089)
## 19.0.54 alpha 2025-05-30
* maint(linux): move TC build scripts to `resources/teamcity/linux` (#14061)
## 19.0.53 alpha 2025-05-29
* fix(developer): ensure that kmc doesn't skip validate phase of kmc-ldml (#14068)
## 19.0.52 alpha 2025-05-28
* feat(developer): line numbers for tran compiler (#14037)
* feat(developer): add line numbers to disp compiler (#14048)
* feat(developer): line numbers for layr compiler (#14050)
* feat(developer): line numbers for LocaCompiler (#14054)
* fix(common): properly use newly installed node version (#14057)
* maint(common): update build coordinator and labels for `resources/teamcity` changes (#14047)
## 19.0.51 alpha 2025-05-27
* maint: add stable-19.0 triggers (#14043)
* maint: add empty 'scripts' tag to package.json to silence warning (#14044)
* fix(linux): fix version variable references in kmconfig (#14032)
## 19.0.50 alpha 2025-05-26
* chore(web): mark async methods in UI components (#14034)
## 19.0.49 alpha 2025-05-25
* feat(developer): revamp line numbers per design doc (#14016)
@ -302,6 +345,32 @@
* refactor(windows): rename `TKeymanMutex.MutexOwned` to `TakeOwnership` and add `ReleaseOwnership` (#13168)
* chore: increment to alpha 19.0 (#13187)
## 18.0.238 stable 2025-06-04
* fix(developer): ldml: make sure .run() calls compile() for validation (#14071)
* maint(linux): fix typo of temporary dependency package (#14085)
* maint(linux): update to new version of gha-ubuntu-packaging (#14096)
* maint(mac): make make-km-dmg.sh less quiet (#14106)
## 18.0.237 stable 2025-05-30
* maint(android): Update FirstVoices launcher icon and red theme (#13917)
* fix(web): empty layout fontSize property should default to 1em (#13960)
* fix(linux): use correct header guard define in OrderedOutputDevice.h (#13963)
* chore(linux): Update debian changelog (#13888)
* fix(mac): handle PackageInfo section in kmp.inf file (#13957)
* feat(android): log active keyboard, model IDs with reported errors (#14003)
* chore(ios): FirstVoices certificate update (#14005)
* feat(ios): log active keyboard, model IDs with reported errors (#14006)
* fix(linux): configurable path in `.service` file when using basu (#13999)
* maint(linux): add `--no-werror` option to not report warnings as errors (#14000)
* fix(windows): use json file name against cache folder and ename Enter and Exit methods for TState Object (#14010)
* chore(ios): update FirstVoices AppIcon (#14011)
* maint: change builder_completion script to a .inc.sh (#14008)
* fix(windows): only breadcrumb unexpected state on first run (#14026)
* fix(windows): skip sending handle kmshell events to upgrade state machine for keyboard install modes (#14028)
* fix(windows): fix sentry error message reporting (#14029)
## 18.0.236 stable 2025-05-12
* chore(linux): Update debian changelog (#13737)

View file

@ -1 +1 @@
19.0.50
19.0.58

3
android/.gitignore vendored
View file

@ -57,6 +57,9 @@ KMAPro/**/assets/*.kmp
# Play Store release notes (copied from whatsnew.md)
KMAPro/kMAPro/src/main/play/release-notes/**/*.txt
# Release artifacts
upload/
# Eclipse files
.project
.settings/

View file

@ -15,6 +15,7 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "$KEYMAN_ROOT/resources/shellHelperFunctions.sh"
. "$KEYMAN_ROOT/resources/build/build-utils-ci.inc.sh"
. "$KEYMAN_ROOT/resources/build/zip.inc.sh"
################################ Main script ################################
@ -37,6 +38,7 @@ builder_describe \
build \
test \
"publish Publishes symbols to Sentry and the APKs to the Play Store." \
"archive Copy release artifacts to upload/ and rsync to downloads.keyman" \
"--ci+ Deprecated build option. Remove in 20.0" \
--upload-sentry+ \
":engine=KMEA Keyman Engine for Android" \
@ -70,4 +72,51 @@ function do_test_help() {
builder_run_action test:help do_test_help
builder_run_child_actions publish
builder_run_child_actions publish
# Copy release artifacts to upload/ and rsync to downloads.keyman.com
if builder_start_action archive; then
UPLOAD_PATH="$KEYMAN_ROOT/android/upload/${KEYMAN_VERSION}"
KEYMAN_ENGINE_ANDROID_ZIP="keyman-engine-android-${KEYMAN_VERSION}.zip"
KEYMAN_APK="keyman-${KEYMAN_VERSION}.apk"
FIRSTVOICES_APK="firstvoices-${KEYMAN_VERSION}.apk"
ZIP_FILE="${UPLOAD_PATH}/${KEYMAN_ENGINE_ANDROID_ZIP}"
ZIP_FLAGS=("-q" "-r") # quiet, recursive
mkdir -p "${UPLOAD_PATH}"
# Create Keyman Engine for Android archive
builder_echo "Copying Keyman Engine for Android into ${UPLOAD_PATH}..."
cd "${UPLOAD_PATH}"
cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/libs/keyman-engine.aar" ./
add_zip_files "${ZIP_FILE}" "${ZIP_FLAGS[@]}" "keyman-engine.aar"
rm -f "keyman-engine.aar"
builder_echo "Copying Keyman Engine for Android Sample projects into ${UPLOAD_PATH}..."
cp -rf "${KEYMAN_ROOT}/android/Samples" ./
add_zip_files "${ZIP_FILE}" "-x@../../zip-excludes" "${ZIP_FLAGS[@]}" "Samples"
rm -rf "Samples"
# Copy release APK
cp "${KEYMAN_ROOT}/android/KMAPro/kMAPro/build/outputs/apk/release/${KEYMAN_APK}" ./
# FirstVoices app
if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then
cp "${KEYMAN_ROOT}/oem/firstvoices/android/app/build/outputs/apk/release/${FIRSTVOICES_APK}" ./
fi
#
# Write download info files
#
cd "${UPLOAD_PATH}"
write_download_info "Keyman Engine for Android" "${KEYMAN_ENGINE_ANDROID_ZIP}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
write_download_info "Keyman for Android" "${KEYMAN_APK}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
if [ "${RELEASE_OEM_FIRSTVOICES-false}" = true ]; then
write_download_info "FirstVoices Keyboards" "${FIRSTVOICES_APK}" "${KEYMAN_VERSION}" "${KEYMAN_TIER}" "android"
fi
builder_finish_action success archive
fi

1
android/zip-excludes Normal file
View file

@ -0,0 +1 @@
Samples/*/build.sh

View file

@ -3,6 +3,7 @@
"description": "Common test resources for Keyman's JS & TS-based components & products",
"type": "module",
"license": "MIT",
"scripts": {},
"devDependencies": {
"@keymanapp/resources-gosh": "*",
"typescript": "^5.4.5"

View file

@ -11,6 +11,7 @@
"/build/"
],
"license": "MIT",
"scripts": {},
"type": "module",
"devDependencies": {
"typescript": "^5.4.5"

View file

@ -1,5 +1,5 @@
import { constants } from '@keymanapp/ldml-keyboard-constants';
import { DependencySections, StrsItem, UsetItem } from './kmx-plus.js';
import { DependencySections, StrsItem, StrsOptions, UsetItem } from './kmx-plus.js';
import { ElementParser, ElementSegment, ElementType } from '../../ldml-keyboard/pattern-parser.js';
import * as util from '../../util/util.js';
import MATCH_HEX_ESCAPE = util.MATCH_HEX_ESCAPE;
@ -33,7 +33,7 @@ export class ElementString extends Array<ElemElement> {
* @param source if a string array, does not get reinterpreted as UnicodeSet. This is used with vars, etc. Or pass `["str"]` for an explicit 1-element elem.
* If it is a string, will be interpreted per reorder element rules.
*/
static fromStrings(sections: DependencySections, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string) : ElementString {
static fromStrings(sections: DependencySections, options: StrsOptions, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string) : ElementString {
// the returned array
const array = new ElementString();
if(!source) {
@ -81,21 +81,21 @@ export class ElementString extends Array<ElemElement> {
// error. So we can just exit here.
return null; // UnicodeSet error
}
const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges);
const uset = sections.usetparser.parseUnicodeSet(item.segment, needRanges, options?.x);
if (!uset) {
return null; // UnicodeSet error already thrown
return null; // UnicodeSet error already added to callback
}
elem.uset = sections.uset.allocUset(uset, sections);
elem.value = sections.strs.allocString('', {singleOk: true}); // no string
elem.uset = sections.uset.allocUset(uset, sections, options?.x);
elem.value = sections.strs.allocString('', {...options, singleOk: true}); // no string
} else if (item.type === ElementType.codepoint || item.type === ElementType.escaped || item.type === ElementType.string) {
// some kind of a string
let str = item.segment;
if (item.type === ElementType.escaped && !MATCH_HEX_ESCAPE.test(str)) {
str = unescapeOneQuadString(str);
// TODO-LDML: any other escape forms here?
elem.value = sections.strs.allocString(str, { singleOk: true });
elem.value = sections.strs.allocString(str, { ...options, singleOk: true });
} else {
elem.value = sections.strs.allocString(str, { unescape: true, singleOk: true });
elem.value = sections.strs.allocString(str, { ...options, unescape: true, singleOk: true });
}
// Now did we end up with one char or no?
if (elem.value.isOneChar) {

View file

@ -41,14 +41,14 @@ export class Elem extends Section {
strings: ElementString[] = [];
constructor(sections: DependencySections) {
super();
this.strings.push(ElementString.fromStrings(sections, '')); // C7043: null element string
this.strings.push(ElementString.fromStrings(sections, {}, '')); // C7043: null element string
}
/**
* @param source if a string array, does not get reinterpreted as UnicodeSet. This is used with vars, etc. Or pass `["str"]` for an explicit 1-element elem.
* If it is a string, will be interpreted per reorder element ruls.
*/
allocElementString(sections: DependencySections, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string): ElementString {
const s = ElementString.fromStrings(sections, source, order, tertiary, tertiary_base, prebase);
allocElementString(sections: DependencySections, options: StrsOptions, source: string | string[], order?: string, tertiary?: string, tertiary_base?: string, prebase?: string): ElementString {
const s = ElementString.fromStrings(sections, options, source, order, tertiary, tertiary_base, prebase);
if (!s) return s;
let result = this.strings.find(item => item.isEqual(s));
if(result === undefined) {
@ -135,6 +135,18 @@ export class StrsItem {
isEqual(a: StrsItem): boolean {
return a.value === this.value && a.char === this.char;
}
private _context: any = undefined;
/** add any context from the options to this strsitem */
setContext(opts?: StrsOptions) {
// At present, there's only a single piece of context available
this._context = this._context || opts?.x;
}
get context() : any {
return this._context;
}
};
/**
@ -161,6 +173,8 @@ export interface StrsOptions {
nfd?: boolean;
/** string can be stored as a single CharStrsItem, not in strs table. */
singleOk?: boolean;
/** optional context */
x?: any;
};
export class Strs extends Section {
@ -192,6 +206,8 @@ export class Strs extends Section {
result = new StrsItem(s);
this.strings.push(result);
}
// give an option to set the context
result.setContext(opts);
return result;
}
@ -200,7 +216,7 @@ export class Strs extends Section {
s = s ?? '';
// type check everything else
if (typeof s !== 'string') {
throw new Error('alloc_string: s must be a string, undefined, or null.');
throw new Error(`Internal Error: processString: s must be a string, undefined, or null, not ${typeof s} ${s}`);
}
// substitute variables
if (opts?.stringVariables) {
@ -358,11 +374,13 @@ export class Vars extends Section {
export class VarsItem extends Section {
id: StrsItem;
value: StrsItem;
x?: any;
constructor(id: string, value: string, sections: DependencySections) {
constructor(id: string, value: string, sections: DependencySections, x?: any) {
super();
this.id = sections.strs.allocString(id);
this.value = sections.strs.allocString(value, {unescape: true});
this.x = x;
}
valid() : boolean {
@ -371,8 +389,8 @@ export class VarsItem extends Section {
};
export class UnicodeSetItem extends VarsItem {
constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser) {
super(id, value, sections);
constructor(id: string, value: string, sections: DependencySections, usetparser: UnicodeSetParser, x?: any) {
super(id, value, sections, x);
const needRanges = sections.usetparser.sizeUnicodeSet(value);
if (needRanges >= 0) {
this.unicodeSet = sections.usetparser.parseUnicodeSet(value, needRanges);
@ -385,9 +403,9 @@ export class UnicodeSetItem extends VarsItem {
};
export class SetVarItem extends VarsItem {
constructor(id: string, value: string[], sections: DependencySections) {
super(id, value.join(' '), sections);
this.items = sections.elem.allocElementString(sections, value);
constructor(id: string, value: string[], sections: DependencySections, x?: any) {
super(id, value.join(' '), sections, x);
this.items = sections.elem.allocElementString(sections, {x}, value);
}
items: ElementString; // element string array
valid() : boolean {
@ -396,8 +414,8 @@ export class SetVarItem extends VarsItem {
};
export class StringVarItem extends VarsItem {
constructor(id: string, value: string, sections: DependencySections) {
super(id, value, sections);
constructor(id: string, value: string, sections: DependencySections, x?: any) {
super(id, value, sections, x);
}
// no added fields
};
@ -435,7 +453,7 @@ export class Tran extends Section {
};
export class UsetItem {
constructor(public uset: UnicodeSet, public str: StrsItem) {
constructor(public uset: UnicodeSet, public str: StrsItem, public x?: any) {
}
compareTo(other: UsetItem) : number {
return this.str.compareTo(other.str);
@ -444,11 +462,11 @@ export class UsetItem {
export class Uset extends Section {
usets: UsetItem[] = [];
allocUset(set: UnicodeSet, sections: DependencySections) : UsetItem {
allocUset(set: UnicodeSet, sections: DependencySections, x?: any) : UsetItem {
// match the same pattern
let result = this.usets.find(s => set.pattern == s.uset.pattern);
if (result === undefined) {
result = new UsetItem(set, sections.strs.allocString(set.pattern));
result = new UsetItem(set, sections.strs.allocString(set.pattern), x);
this.usets.push(result);
}
return result;

View file

@ -8,13 +8,13 @@ export interface UnicodeSetParser {
* @param pattern string to parse such as `[a-z]`
* @param rangeCount number of ranges to allow for
*/
parseUnicodeSet(pattern: string, rangeCount: number) : UnicodeSet | null;
parseUnicodeSet(pattern: string, rangeCount: number, x?: any) : UnicodeSet | null;
/**
* Calculate the number of ranges in a UnicodeSet
* @param pattern string to parse such as `[a-z]`
* @returns number of ranges, or -1 (with callback-reported err) on err
*/
sizeUnicodeSet(pattern: string) : number;
sizeUnicodeSet(pattern: string, x?: any) : number;
}
/**

View file

@ -91,12 +91,12 @@ describe('Test of ElementString file', () => {
});
describe('Test of fromStrings()', () => {
it('returns an empty ElementString if source is null', () => {
const es = ElementString.fromStrings({}, null);
const es = ElementString.fromStrings({}, {}, null);
assert.deepEqual(es, new ElementString());
});
it('can create an ElementString from a string array', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(sections, ["𐌰", "𐌱", "𐌲"]);
const actual = ElementString.fromStrings(sections, {}, ["𐌰", "𐌱", "𐌲"]);
const expected = [
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
@ -106,7 +106,7 @@ describe('Test of ElementString file', () => {
});
it('can create an ElementString from a string', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(sections, "𐌰𐌱𐌲");
const actual = ElementString.fromStrings(sections, {}, "𐌰𐌱𐌲");
const expected = [
initElemElement(GOTHIC_A),
initElemElement(GOTHIC_B),
@ -118,6 +118,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
"1 2 3",
);
@ -132,6 +133,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
"1",
);
@ -146,6 +148,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
"1 2",
);
@ -160,6 +163,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
"1 A 3",
);
@ -174,6 +178,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
"1 2 3",
@ -189,6 +194,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
"1",
@ -204,6 +210,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
"1 2",
@ -219,6 +226,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
"1 A 3",
@ -234,6 +242,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -250,6 +259,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -266,6 +276,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -282,6 +293,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -299,6 +311,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -316,6 +329,7 @@ describe('Test of ElementString file', () => {
sections.strs.allocString = stubStrsAllocString_Char;
const actual = ElementString.fromStrings(
sections,
{},
"𐌰𐌱𐌲",
null,
null,
@ -333,7 +347,7 @@ describe('Test of ElementString file', () => {
ElementParser.segment = stubElementParserSegment_Uset;
sections.strs.allocString = stubStrsAllocString_Str;
sections.uset.allocUset = stubUsetAllocUset;
const actual = ElementString.fromStrings(sections, "[𐌰-𐍊]");
const actual = ElementString.fromStrings(sections, {}, "[𐌰-𐍊]");
const expected = [
initElemElement(
new StrsItem(''),
@ -348,17 +362,17 @@ describe('Test of ElementString file', () => {
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, "[𐌰-𐍊]"));
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, "[𐌰-𐍊]"));
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 actual = ElementString.fromStrings(sections, {}, "\\ud800\\udf30");
const expected = [
initElemElement(HI_GOTHIC_A),
initElemElement(LO_GOTHIC_A),
@ -367,7 +381,7 @@ describe('Test of ElementString file', () => {
});
it('can handle ElemElement of string type', () => {
sections.strs.allocString = stubStrsAllocString_Str;
const actual = ElementString.fromStrings(sections, ["𐌰𐌱𐌲",]);
const actual = ElementString.fromStrings(sections, {}, ["𐌰𐌱𐌲",]);
const expected = [
initElemElement(
new StrsItem("𐌰𐌱𐌲"),

View file

@ -46,6 +46,8 @@
#define LDML_KEYS_MOD_OTHER 0x10000
#define LDML_KEYS_MOD_SHIFT 0x10
#define LDML_LAYR_LIST_HARDWARE_TOUCH "touch"
#define LDML_LAYR_MAX_MINDEVICEWIDTH 0x3E7
#define LDML_LAYR_MIN_MINDEVICEWIDTH 0x1
#define LDML_LENGTH_BKSP 0xC
#define LDML_LENGTH_BKSP_ITEM 0x10
#define LDML_LENGTH_DISP 0x10

View file

@ -362,6 +362,16 @@ class Constants {
*/
readonly length_layr_key = 4;
/**
* Minimum allowed minDeviceWidth for a layer list
*/
readonly layr_min_minDeviceWidth = 1;
/**
* Maximum allowed minDeviceWidth for a layer list
*/
readonly layr_max_minDeviceWidth = 999;
/* ------------------------------------------------------------------
* list section
------------------------------------------------------------------ */

View file

@ -13,6 +13,7 @@
"license": "MIT",
"type": "module",
"main": "build/keyman_core_ldml.js",
"scripts": {},
"repository": {
"type": "git",
"url": "git+https://github.com/keymanapp/keyman.git"

View file

@ -2,5 +2,6 @@
"name": "@keymanapp/api-header-extractor",
"private": true,
"type": "module",
"scripts": {},
"main": "build/src/index.js"
}

View file

@ -1,4 +1,6 @@
import { CompilerCallbacks } from "./compiler-callbacks.js";
import { ObjectWithMetadata } from "./symbol-utils.js";
import { KeymanXMLReader, XML_FILENAME_SYMBOL } from "./xml-utils.js";
/**
* Abstract interface for compiler error and warning messages
@ -206,6 +208,27 @@ export class CompilerError {
}
return null;
}
/**
* Get an offset from o and set event's offset field
* @param event a compiler event, such as from functions in this class
* @param x any object parsed from XML or with the XML_META_DATA_SYMBOL symbol copied over
* @returns modified event object
*/
public static setFromMetadata(event: CompilerEvent, x?: ObjectWithMetadata): CompilerEvent {
if(x) {
const metadata = KeymanXMLReader.getMetaData(x) || {};
const offset = metadata?.startIndex;
if (offset) {
event.offset = offset;
}
const filename = event.filename || metadata[XML_FILENAME_SYMBOL];
if (filename) {
event.filename = filename;
}
}
return event;
}
};
/** @deprecated use `CompilerError.severity` instead */
@ -423,6 +446,40 @@ export function dedentCompilerMessageDetail(event: CompilerEvent) {
return (event.detail ?? '').replace(/^[ ]+/gm, '');
}
/**
* Convenience function for constructing CompilerEvents with line numbers.
* Use it as below: (abbreviated as mx())
*
* ```js
* // Note: Indentation makes "InvalidScanCode" line up thrice
* static ERROR_InvalidScanCode = SevError | 0x0009;
* // Note:
* // 1. All parameters are passed in 'o', the context object is only used for context even if
* // it contains redundant info.
* // 2. No code execution within the arrow function other than the 'mx' call, string interpolation,
* // with `${def(o.property)}` as the max complexity of interpolation.
* static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, x: ObjectWithMetadata) => mx(
* this.ERROR_InvalidScanCode, x,
* `Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`,
* // Note: If detail is omitted, leave the trailing comma on the prior line to leave room for it
* `…additional markdown detail…`
* );
* ```
*
* @param code Unique numeric value of the event
* @param message A short description of the error presented to the user
* @param context Object to be used as a source for line number information
* @param detail Detailed Markdown-formatted description of the error
* including references to documentation, remediation options.
* @see CompilerMessageSpec
* @returns the event
*/
export function CompilerMessageObjectSpec(code: number, context: ObjectWithMetadata, message: string, detail?: string): CompilerEvent {
let evt = CompilerMessageSpec(code, message, detail); // constructs raw message
evt = CompilerError.setFromMetadata(evt, context); // updates with offset from context
return evt;
};
export const CompilerMessageDef = (param: any) => String(param ?? `<param>`);
export const CompilerMessageSpecWithException = (code: number, message: string, exceptionVar: any, detail?: string) : CompilerEvent => ({

View file

@ -48,7 +48,7 @@ export {
export { defaultCompilerOptions, CompilerBaseOptions, CompilerOptions, CompilerEvent, CompilerErrorNamespace,
CompilerErrorSeverity, CompilerCallbackOptions,
CompilerError, CompilerMessageSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, compilerErrorSeverityName,
CompilerError, CompilerMessageSpec, CompilerMessageObjectSpec, CompilerMessageSpecWithException, compilerErrorSeverity, CompilerErrorMask, compilerErrorSeverityName,
compilerErrorFormatCode, CompilerMessageDef,
compilerLogLevelToSeverity, CompilerLogLevel, compilerEventFormat, ALL_COMPILER_LOG_LEVELS,
ALL_COMPILER_LOG_FORMATS, CompilerLogFormat,
@ -68,7 +68,7 @@ export { UrlSubpathCompilerCallback } from './utils/UrlSubpathCompilerCallback.j
export { DeveloperUtilsMessages } from './developer-utils-messages.js';
export * as SourceFilenamePatterns from './source-filename-patterns.js';
export { KeymanXMLType, KeymanXMLWriter, KeymanXMLReader, KeymanXMLMetadata, XML_FILENAME_SYMBOL } from './xml-utils.js';
export { SymbolUtils } from './symbol-utils.js';
export { SymbolUtils, ObjectWithMetadata } from './symbol-utils.js';
export * as LineUtils from './line-utils.js';
export * as GitHubUrls from './github-urls.js';
export * as CloudUrls from './cloud-urls.js';

View file

@ -6,6 +6,14 @@
* Utilities for manipulating Symbol properties
*/
/**
* Any object with metadata, for example for line number errs.
* Defined as 'any' here to reduce noise on the client side.
* @see {@link KeymanXMLReader.getMetaData()}
*/
export type ObjectWithMetadata = any;
export class SymbolUtils {
/**
* Copy symbols shallowly from 'from' onto 'onto'
@ -13,8 +21,8 @@ export class SymbolUtils {
* @param from source for symbols
* @returns the onto object
*/
public static copySymbols<T>(onto: T, from: any): T {
const o = onto as any;
public static copySymbols<T>(onto: T, from: ObjectWithMetadata): T {
const o = onto as ObjectWithMetadata;
for (const sym of Object.getOwnPropertySymbols(from)) {
o[sym] = from[sym];
}

View file

@ -9,7 +9,8 @@ TODO: implement additional interfaces:
import { VisualKeyboard, KvkFileReader, LdmlKeyboardTypes, KeymanFileTypes, KvkFileWriter } from '@keymanapp/common-types';
import {
CompilerCallbacks, CompilerEvent, CompilerOptions, KeymanCompiler, KeymanCompilerArtifacts,
KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader
KeymanCompilerArtifactOptional, KeymanCompilerResult, KeymanCompilerArtifact, KvksFileReader,
CompilerError
} from '@keymanapp/developer-utils';
import * as Osk from './osk.js';
import loadWasmHost from '../import/kmcmplib/wasm-host.js';
@ -529,7 +530,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
* @param rangeCount - number of ranges to allocate
* @returns UnicodeSet accessor object, or null on failure
*/
public parseUnicodeSet(pattern: string, rangeCount: number) : LdmlKeyboardTypes.UnicodeSet | null {
public parseUnicodeSet(pattern: string, rangeCount: number, x?: any) : LdmlKeyboardTypes.UnicodeSet | null {
if(!this.verifyInitialized()) {
/* c8 ignore next 2 */
// verifyInitialized will set a callback if needed
@ -562,7 +563,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
// rc is negative: it's an error code.
this.wasmExports.free(buf);
// translate error code into callback
this.callbacks.reportMessage(getUnicodeSetError(rc));
this.callbacks.reportMessage(getUnicodeSetError(rc, x));
return null;
}
}
@ -570,7 +571,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
/**
* @internal
*/
public sizeUnicodeSet(pattern: string) : number {
public sizeUnicodeSet(pattern: string, x?: any) : number {
if(!this.verifyInitialized()) {
/* c8 ignore next 2 */
return null;
@ -582,7 +583,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
if (rc >= 0) {
return rc;
} else {
this.callbacks.reportMessage(getUnicodeSetError(rc));
this.callbacks.reportMessage(getUnicodeSetError(rc, x));
return -1;
}
}
@ -593,7 +594,7 @@ export class KmnCompiler implements KeymanCompiler, LdmlKeyboardTypes.UnicodeSet
* @param rc parseUnicodeSet error code
* @returns the compiler event
*/
function getUnicodeSetError(rc: number) : CompilerEvent {
function getUnicodeSetError(rc: number, x?: any) : CompilerEvent {
// from kmcmplib.h
const KMCMP_ERROR_SYNTAX_ERR = -1;
const KMCMP_ERROR_HAS_STRINGS = -2;
@ -601,16 +602,16 @@ function getUnicodeSetError(rc: number) : CompilerEvent {
const KMCMP_FATAL_OUT_OF_RANGE = -4;
switch(rc) {
case KMCMP_ERROR_SYNTAX_ERR:
return KmnCompilerMessages.Error_UnicodeSetSyntaxError();
return KmnCompilerMessages.Error_UnicodeSetSyntaxError(x);
case KMCMP_ERROR_HAS_STRINGS:
return KmnCompilerMessages.Error_UnicodeSetHasStrings();
return KmnCompilerMessages.Error_UnicodeSetHasStrings(x);
case KMCMP_ERROR_UNSUPPORTED_PROPERTY:
return KmnCompilerMessages.Error_UnicodeSetHasProperties();
return KmnCompilerMessages.Error_UnicodeSetHasProperties(x);
case KMCMP_FATAL_OUT_OF_RANGE:
return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange();
return KmnCompilerMessages.Fatal_UnicodeSetOutOfRange(x);
default:
/* c8 ignore next */
return KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`});
return CompilerError.setFromMetadata(KmnCompilerMessages.Fatal_UnexpectedException({e: `Unexpected UnicodeSet error code ${rc}`}), x);
}
}

View file

@ -1,6 +1,6 @@
import { KeyAddress } from "../kmw-compiler/validate-layout-file.js";
import { kmnfile } from "../kmw-compiler/compiler-globals.js";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls } from "@keymanapp/developer-utils";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerEvent, CompilerMessageSpec as m, CompilerMessageObjectSpec as mx, CompilerMessageDef as def, CompilerMessageSpecWithException, KeymanUrls, ObjectWithMetadata } from "@keymanapp/developer-utils";
const Namespace = CompilerErrorNamespace.KmnCompiler;
const SevInfo = CompilerErrorSeverity.Info | Namespace;
@ -12,6 +12,9 @@ const SevFatal = CompilerErrorSeverity.Fatal | Namespace;
// For messages from the KeymanWeb compiler, we need to construct our messages
// slightly differently. This could be refactored in the future, as it is not
// obvious which messages should use which function.
// Specifically, this could perhaps use CompilerFileCallbacks which provides a
// default filename.
// Then, there could be a CompilerMessageSpecWithLine
const mw = (code: number, message: string, o?: {filename?: string, line?: number}) : CompilerEvent => ({
...m(code, message),
filename: o?.filename ?? kmnfile,
@ -104,9 +107,8 @@ export class KmnCompilerMessages {
);
static FATAL_UnicodeSetOutOfRange = SevFatal | 0x904;
static Fatal_UnicodeSetOutOfRange = () => CompilerMessageSpecWithException(
this.FATAL_UnicodeSetOutOfRange,
null,
static Fatal_UnicodeSetOutOfRange = (x?: ObjectWithMetadata) => mx(
this.FATAL_UnicodeSetOutOfRange, x,
`UnicodeSet buffer was too small`,
`Raised when caller to UnicodeSet functions provides an invalid buffer. If
you experience this error, it should be reported to the Keyman team for
@ -116,8 +118,8 @@ export class KmnCompilerMessages {
// TODO: rename the following functions to Error_UsetHasStrings etc
static ERROR_UnicodeSetHasStrings = SevError | 0x905;
static Error_UnicodeSetHasStrings = () => m(
this.ERROR_UnicodeSetHasStrings,
static Error_UnicodeSetHasStrings = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetHasStrings, x,
`uset contains strings, not allowed`,
`The provided uset uses multi-character strings, (\`{}\` notation, e.g.
\`[żġħ{ie}{għ}]\`. ). Although full UnicodeSets support strings, LDML
@ -128,8 +130,8 @@ export class KmnCompilerMessages {
);
static ERROR_UnicodeSetHasProperties = SevError | 0x906;
static Error_UnicodeSetHasProperties = () => m(
this.ERROR_UnicodeSetHasProperties,
static Error_UnicodeSetHasProperties = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetHasProperties, x,
`uset contains properties, not allowed`,
`The provided uset uses property notation (\`\\p{…}\` or \`[:…:]\`). LDML
keyboards do not support Unicode properties in usets, because that would
@ -140,8 +142,8 @@ export class KmnCompilerMessages {
);
static ERROR_UnicodeSetSyntaxError = SevError | 0x907;
static Error_UnicodeSetSyntaxError = () => m(
this.ERROR_UnicodeSetSyntaxError,
static Error_UnicodeSetSyntaxError = (x?: ObjectWithMetadata) => mx(
this.ERROR_UnicodeSetSyntaxError, x,
`uset had a Syntax Error while parsing`,
`The provided uset has a syntax error and could not be parsed. Verify the
format of the uset against the specification.

View file

@ -136,7 +136,7 @@ export class LdmlKeyboardCompiler implements KeymanCompiler {
if (!source) {
return null;
}
const kmx = await this.compile(source);
const kmx = await this.compile(source, true);
if (!kmx) {
return null;
}
@ -352,6 +352,8 @@ export class LdmlKeyboardCompiler implements KeymanCompiler {
* Transforms in-memory LDML keyboard xml file to an intermediate
* representation of a .kmx file.
* @param source - in-memory representation of LDML keyboard xml file
* @param postValidate - pass true if sections should run a 'validate' phase at the very end.
* Set this to true if you aren't calling validate() separately.
* @returns KMXPlusFile intermediate file
*/
public async compile(source: LDMLKeyboardXMLSourceFile, postValidate?: boolean): Promise<KMXPlus.KMXPlusFile> {

View file

@ -12,9 +12,10 @@ import { SubstitutionUse, Substitutions } from "./substitution-tracker.js";
export class DispCompiler extends SectionCompiler {
static validateSubstitutions(keyboard: LDMLKeyboard.LKKeyboard, st : Substitutions): boolean {
keyboard.displays?.display?.forEach(({ display, output }) => {
st.addStringAndMarkerSubstitution(SubstitutionUse.match, output);
st.addStringSubstitution(SubstitutionUse.emit, display);
keyboard.displays?.display?.forEach((e) => {
const { display, output } = e;
st.addStringAndMarkerSubstitution(SubstitutionUse.match, output, e);
st.addStringSubstitution(SubstitutionUse.emit, display, e);
});
// no marker references in 'id'
return true;
@ -31,20 +32,21 @@ export class DispCompiler extends SectionCompiler {
const ids = new Set();
if (this.keyboard3.displays?.display) {
for (const { output, keyId } of this.keyboard3.displays?.display) {
for (const display of this.keyboard3.displays?.display) {
const { output, keyId } = display;
if ((output && keyId) || (!output && !keyId)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayNeedsToOrId({ output, keyId }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: display.display }, display));
return false;
} else if (output) {
if (tos.has(output)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ output }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ display: display.display }, display));
return false;
} else {
tos.add(output);
}
} else if (keyId) {
if (ids.has(keyId)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ keyId }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DisplayIsRepeated({ display: display.display }, display));
return false;
} else {
ids.add(keyId);
@ -60,7 +62,7 @@ export class DispCompiler extends SectionCompiler {
const result = new Disp();
// displayOptions
result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, {unescape: true});
result.baseCharacter = sections.strs.allocString(this.keyboard3.displays?.displayOptions?.baseCharacter, { unescape: true, x: this.keyboard3?.displays?.displayOptions });
// displays
result.disps = this.keyboard3.displays?.display.map(display => ({
@ -68,11 +70,13 @@ export class DispCompiler extends SectionCompiler {
stringVariables: true,
markers: true,
unescape: true,
x: display,
}, sections),
id: sections.strs.allocString(display.keyId), // not escaped, not substituted
id: sections.strs.allocString(display.keyId, { x: display }), // not escaped, not substituted
display: sections.strs.allocString(display.display, {
stringVariables: true,
unescape: true,
x: display,
}, sections),
})) || []; // TODO-LDML: need coverage for the []

View file

@ -1,7 +1,7 @@
import { SectionIdent, constants } from '@keymanapp/ldml-keyboard-constants';
import { SectionCompiler } from "./section-compiler.js";
import { util, KMXPlus, LdmlKeyboardTypes } from "@keymanapp/common-types";
import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils";
import { CompilerCallbacks, LDMLKeyboard, ObjectWithMetadata } from "@keymanapp/developer-utils";
import { VarsCompiler } from './vars.js';
import { LdmlCompilerMessages } from './ldml-compiler-messages.js';
@ -33,13 +33,26 @@ export class StrsCompiler extends EmptyCompiler {
public postValidate(section?: KMXPlus.Section): boolean {
const strs = <KMXPlus.Strs>section;
/** attempt to find a context object for the string */
function findContextForString(s: string): ObjectWithMetadata {
// try exact match
for(const str of strs.strings) {
if (str.value == s) return str.context;
}
// try substring match
for(const str of strs.strings) {
if (str.value.includes(s)) return str.context;
}
return null;
}
if (strs) {
const badStringAnalyzer = new util.BadStringAnalyzer();
const CONTAINS_MARKER_REGEX = new RegExp(LdmlKeyboardTypes.MarkerParser.ANY_MARKER_MATCH);
for (let s of strs.allProcessedStrings.values()) {
// stop at the first denormalized string
if (!util.isNormalized(s)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_StringDenorm({s}));
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_StringDenorm({s}, findContextForString(s)));
}
// replace all \\uXXXX with the actual code point.
// this lets us analyze whether there are PUA, unassigned, etc.
@ -63,16 +76,19 @@ export class StrsCompiler extends EmptyCompiler {
const illegals = m.get(util.BadStringType.illegal);
if (puas) {
const [count, lowestCh] = [puas.size, Array.from(puas.values()).sort((a, b) => a - b)[0]];
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_PUACharacters({ count, lowestCh }))
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_PUACharacters({ count, lowestCh: util.describeCodepoint(lowestCh) },
findContextForString(String.fromCodePoint(lowestCh))));
}
if (unassigneds) {
const [count, lowestCh] = [unassigneds.size, Array.from(unassigneds.values()).sort((a, b) => a - b)[0]];
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_UnassignedCharacters({ count, lowestCh }))
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_UnassignedCharacters({ count, lowestCh: util.describeCodepoint(lowestCh) },
findContextForString(String.fromCodePoint(lowestCh))));
}
if (illegals) {
// do this last, because we will return false.
const [count, lowestCh] = [illegals.size, Array.from(illegals.values()).sort((a, b) => a - b)[0]];
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalCharacters({ count, lowestCh }))
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalCharacters({ count, lowestCh: util.describeCodepoint(lowestCh) },
findContextForString(String.fromCodePoint(lowestCh))));
return false;
}
}

View file

@ -37,7 +37,7 @@ export class KeysCompiler extends SectionCompiler {
for (const keyId of usedKeys.values()) {
const key = keyBag.get(keyId);
if (!key) continue; // key not found is handled elsewhere.
st.addStringAndMarkerSubstitution(SubstitutionUse.emit, key.output);
st.addStringAndMarkerSubstitution(SubstitutionUse.emit, key.output, key);
}
return true;
}
@ -324,15 +324,16 @@ export class KeysCompiler extends SectionCompiler {
// allocate the in-memory <flick id=…>
const flicks: KeysFlicks = new KeysFlicks(
sections.strs.allocString(flickId)
sections.strs.allocString(flickId, { x: flick })
);
// add data from each segment
for (const { keyId, directions } of flick.flickSegment) {
const keyIdStr = sections.strs.allocString(keyId);
for (const flickSegment of flick.flickSegment) {
const { keyId, directions } = flickSegment;
const keyIdStr = sections.strs.allocString(keyId, { x: flickSegment });
const directionsList: ListItem = sections.list.allocListFromSpaces(
directions,
{ },
{ x: flickSegment },
sections);
flicks.flicks.push({
directions: directionsList,
@ -369,20 +370,20 @@ export class KeysCompiler extends SectionCompiler {
if (!!gap) {
flags |= constants.keys_key_flags_gap;
}
const id = sections.strs.allocString(key.id);
const id = sections.strs.allocString(key.id, { x: key });
const longPress: ListItem = sections.list.allocListFromSpaces(
longPressKeyIds, {},
longPressKeyIds, { x: key },
sections);
const longPressDefault = sections.strs.allocString(longPressDefaultKeyId,
{},
{ x: key },
sections);
const multiTap: ListItem = sections.list.allocListFromSpaces(
multiTapKeyIds,
{},
{ x: key },
sections);
const keySwitch = sections.strs.allocString(layerId); // 'switch' is a reserved word
const keySwitch = sections.strs.allocString(layerId, { x: key }); // 'switch' is a reserved word
const toRaw = output;
@ -395,6 +396,7 @@ export class KeysCompiler extends SectionCompiler {
unescape: true,
singleOk: true,
nfd: true,
x: key,
},
sections);
if (!to.isOneChar) {
@ -461,7 +463,7 @@ export class KeysCompiler extends SectionCompiler {
const { modifiers } = layer;
if (!validModifier(modifiers)) {
this.callbacks.reportMessage(
LdmlCompilerMessages.Error_InvalidModifier({ modifiers, id: layer.id }, layer)
LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer)
);
valid = false;
}
@ -482,7 +484,7 @@ export class KeysCompiler extends SectionCompiler {
LdmlCompilerMessages.Error_RowOnHardwareLayerHasTooManyKeys({
row: y + 1,
hardware: layers.formId,
modifiers,
modifiers: modifiers || 'none',
}, row)
);
valid = false;

View file

@ -20,27 +20,39 @@ export class LayrCompiler extends SectionCompiler {
let valid = true;
let totalLayerCount = 0;
let hardwareLayers = 0;
// let touchLayers = 0;
let touchLayers = 0;
const deviceWidths = new Set<number>();
this.keyboard3.layers?.forEach((layers) => {
const { formId } = layers;
if (formId === 'touch') {
// touchLayers++;
// multiple touch layers are OK
touchLayers++;
totalLayerCount += layers.layer?.length;
// TODO-LDML: check that widths are distinct
const { minDeviceWidth } = layers;
if (!minDeviceWidth ||
minDeviceWidth < constants.layr_min_minDeviceWidth ||
minDeviceWidth > constants.layr_max_minDeviceWidth ||
Number.isNaN(Number(minDeviceWidth))) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidLayerWidth({minDeviceWidth}, layers));
} else if (deviceWidths.has(minDeviceWidth)) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_DuplicateLayerWidth({minDeviceWidth}, layers));
} else {
deviceWidths.add(minDeviceWidth);
}
} else {
// hardware
hardwareLayers++;
if (hardwareLayers > 1) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_ExcessHardware({formId}));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_ExcessHardware({formId}, layers));
}
}
layers.layer.forEach((layer) => {
const { modifiers } = layer;
totalLayerCount++;
if (!validModifier(modifiers)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers, id: layer.id }, layer));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidModifier({ modifiers }, layer));
valid = false;
}
});
@ -48,7 +60,7 @@ export class LayrCompiler extends SectionCompiler {
if (totalLayerCount === 0) { // TODO-LDML: does not validate touch layers yet
// no layers seen anywhere
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement());
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(this.keyboard3?.layers[0]));
}
return valid;
}
@ -57,13 +69,13 @@ export class LayrCompiler extends SectionCompiler {
const sect = new Layr();
sect.lists = this.keyboard3.layers.map((layers) => {
const hardware = sections.strs.allocString(layers.formId);
const hardware = sections.strs.allocString(layers.formId, {x:layers});
// Already validated in validate
const layerEntries = [];
for (const layer of layers.layer) {
const rows = layer.row.map((row) => {
const erow: LayrRow = {
keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id)),
keys: row.keys.trim().split(/[ \t]+/).map((id) => sections.strs.allocString(id, { x: row })),
};
// include linenumber info for row
return SectionCompiler.copySymbols(erow, row);
@ -72,7 +84,7 @@ export class LayrCompiler extends SectionCompiler {
// push a layer entry for each modifier set
for (const mod of mods) {
layerEntries.push({
id: sections.strs.allocString(layer.id),
id: sections.strs.allocString(layer.id, {x:layer}),
mod,
rows,
});

View file

@ -1,5 +1,4 @@
import { util } from "@keymanapp/common-types";
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageSpec as m, CompilerMessageDef as def, XML_FILENAME_SYMBOL, CompilerEvent, KeymanXMLReader } from '@keymanapp/developer-utils';
import { CompilerErrorNamespace, CompilerErrorSeverity, CompilerMessageObjectSpec as mx, CompilerMessageSpec as m, CompilerMessageDef as def, ObjectWithMetadata } from '@keymanapp/developer-utils';
// const SevInfo = CompilerErrorSeverity.Info | CompilerErrorNamespace.LdmlKeyboardCompiler;
const SevHint = CompilerErrorSeverity.Hint | CompilerErrorNamespace.LdmlKeyboardCompiler;
const SevWarn = CompilerErrorSeverity.Warn | CompilerErrorNamespace.LdmlKeyboardCompiler;
@ -9,50 +8,6 @@ const SevError = CompilerErrorSeverity.Error | CompilerErrorNamespace.LdmlKeyboa
// sub-numberspace for transform errors
const SevErrorTransform = SevError | 0xF00;
/**
* Any object with metadata, for line number errs.
* Could be for example an LKKeys or KMXPlus.KeysKeys object.
* Defined as 'any' here to reduce noise on the client side.
* @see {@link KeymanXMLReader.getMetaData()}
*/
type ObjectWithMetadata = any;
/**
* Convenience function for constructing CompilerEvents with line numbers.
* Use it as below: (abbreviated as mx())
*
* ```js
* // Note: Indentation makes "InvalidScanCode" line up thrice
* static ERROR_InvalidScanCode = SevError | 0x0009;
* // Note:
* // 1. All parameters are passed in 'o', the context object is only used for context even if
* // it contains redundant info.
* // 2. No code execution within the arrow function other than the 'mx' call, string interpolation,
* // with `${def(o.property)}` as the max complexity of interpolation.
* static Error_InvalidScanCode = (o:{id: string, invalidCodeList: string}, x: ObjectWithMetadata) => mx(
* this.ERROR_InvalidScanCode, x,
* `Form '${def(o.id)}' has invalid/unknown scancodes '${def(o.codes)}'`,
* // Note: If detail is omitted, leave the trailing comma on the prior line to leave room for it
* `…additional markdown detail…`
* );
* ```
*
* @param code Unique numeric value of the event
* @param message A short description of the error presented to the user
* @param context Object to be used as a source for line number information
* @param detail Detailed Markdown-formatted description of the error
* including references to documentation, remediation options.
* @see CompilerMessageSpec
* @returns
*/
function CompilerMessageObjectSpec(code: number, context: ObjectWithMetadata, message: string, detail?: string): CompilerEvent {
let evt = m(code, message, detail); // constructs raw message
evt = LdmlCompilerMessages.offset(evt, context); // updates with offset from context
return evt;
};
const mx = CompilerMessageObjectSpec;
/**
* @internal
*/
@ -64,7 +19,10 @@ export class LdmlCompilerMessages {
);
static ERROR_InvalidLocale = SevError | 0x0002;
static Error_InvalidLocale = (o:{tag: string}) => m(this.ERROR_InvalidLocale, `Invalid BCP 47 locale form '${def(o.tag)}'`);
static Error_InvalidLocale = (o:{tag: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidLocale, x,
`Invalid BCP 47 locale form '${def(o.tag)}'`,
);
static ERROR_HardwareLayerHasTooManyRows = SevError | 0x0003;
static Error_HardwareLayerHasTooManyRows = (x?: ObjectWithMetadata) => mx(
@ -75,7 +33,7 @@ export class LdmlCompilerMessages {
static ERROR_RowOnHardwareLayerHasTooManyKeys = SevError | 0x0004;
static Error_RowOnHardwareLayerHasTooManyKeys = (o: { row: number, hardware: string, modifiers: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_RowOnHardwareLayerHasTooManyKeys, x,
`Row #${def(o.row)} on 'hardware' ${def(o.hardware)} layer for modifier ${o.modifiers || 'none'} has too many keys`,
`Row #${def(o.row)} on 'hardware' ${def(o.hardware)} layer for modifier ${def(o.modifiers)} has too many keys`,
);
static ERROR_KeyNotFoundInKeyBag = SevError | 0x0005;
@ -85,16 +43,21 @@ export class LdmlCompilerMessages {
);
static HINT_OneOrMoreRepeatedLocales = SevHint | 0x0006;
static Hint_OneOrMoreRepeatedLocales = () =>
m(this.HINT_OneOrMoreRepeatedLocales, `After minimization, one or more locales is repeated and has been removed`);
static Hint_OneOrMoreRepeatedLocales = (x?: ObjectWithMetadata) => mx(
this.HINT_OneOrMoreRepeatedLocales, x,
`After minimization, one or more locales is repeated and has been removed`,
);
// This is the only allowed use of m() vs mx() in this file, all the others take context.
static ERROR_InvalidFile = SevError | 0x0007;
static Error_InvalidFile = (o:{errorText: string}) =>
m(this.ERROR_InvalidFile, `The source file has an invalid structure: ${def(o.errorText)}`);
static HINT_LocaleIsNotMinimalAndClean = SevHint | 0x0008;
static Hint_LocaleIsNotMinimalAndClean = (o:{sourceLocale: string, locale: string}) =>
m(this.HINT_LocaleIsNotMinimalAndClean, `Locale '${def(o.sourceLocale)}' is not minimal or correctly formatted and should be '${def(o.locale)}'`);
static Hint_LocaleIsNotMinimalAndClean = (o:{sourceLocale: string, locale: string}, x?: ObjectWithMetadata) => mx(
this.HINT_LocaleIsNotMinimalAndClean, x,
`Locale '${def(o.sourceLocale)}' is not minimal or correctly formatted and should be '${def(o.locale)}'`,
);
static ERROR_InvalidScanCode = SevError | 0x0009;
static Error_InvalidScanCode = (o: { codes: string, id: string }, x?: ObjectWithMetadata) => mx(
@ -109,15 +72,16 @@ export class LdmlCompilerMessages {
);
static ERROR_GestureKeyNotFoundInKeyBag = SevError | 0x000B;
static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}, x?: ObjectWithMetadata) =>
mx(
static Error_GestureKeyNotFoundInKeyBag = (o:{keyId: string, parentKeyId: string, attribute: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_GestureKeyNotFoundInKeyBag, x,
`Key '${def(o.keyId)}' not found in key bag, referenced from other '${def(o.parentKeyId)}' in ${def(o.attribute)}`,
);
static HINT_NoDisplayForMarker = SevHint | 0x000C;
static Hint_NoDisplayForMarker = (o: { id: string }) =>
m(this.HINT_NoDisplayForMarker, `Key element with id "${def(o.id)}" has only marker output, but there is no matching display element by output or keyId. Keycap may be blank.`);
static Hint_NoDisplayForMarker = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.HINT_NoDisplayForMarker, x,
`Key element with id "${def(o.id)}" has only marker output, but there is no matching display element by output or keyId. Keycap may be blank.`,
);
static ERROR_InvalidVersion = SevError | 0x000D;
static Error_InvalidVersion = (o: { version: string; }, x?: ObjectWithMetadata) => mx(
@ -134,29 +98,22 @@ export class LdmlCompilerMessages {
);
static ERROR_MustBeAtLeastOneLayerElement = SevError | 0x000E;
static Error_MustBeAtLeastOneLayerElement = () =>
m(this.ERROR_MustBeAtLeastOneLayerElement, `The source file must contain at least one layer element.`);
static Error_MustBeAtLeastOneLayerElement = (x?: ObjectWithMetadata) => mx(
this.ERROR_MustBeAtLeastOneLayerElement, x,
`The source file must contain at least one layer element.`,
);
static HINT_NoDisplayForSwitch = SevHint | 0x000F;
static Hint_NoDisplayForSwitch = (o: { id: string }) =>
m(this.HINT_NoDisplayForSwitch, `Key element with id "${def(o.id)}" is a layer switch key, but there is no matching display element by keyId. Keycap may be blank.`);
/** annotate the to= or id= entry */
private static outputOrKeyId(o:{output?: string, keyId?: string}) {
if (o.output && o.keyId) {
return `output='${o.output}' keyId='${o.keyId}'`;
} else if(o.keyId) {
return `keyId='${o.keyId}'`;
} else if (o.output) {
return `output='${o.output}'`;
} else {
return '';
}
}
static Hint_NoDisplayForSwitch = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.HINT_NoDisplayForSwitch, x,
`Key element with id "${def(o.id)}" is a layer switch key, but there is no matching display element by keyId. Keycap may be blank.`,
);
static ERROR_DisplayIsRepeated = SevError | 0x0010;
static Error_DisplayIsRepeated = (o:{output?: string, keyId?: string}) =>
m(this.ERROR_DisplayIsRepeated, `display ${LdmlCompilerMessages.outputOrKeyId(o)} has more than one display entry.`);
static Error_DisplayIsRepeated = (o:{display?: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DisplayIsRepeated, x,
`display display='${def(o.display)}' refers to the same keyId or output as another entry.`,
);
static ERROR_KeyMissingToGapOrSwitch = SevError | 0x0011;
static Error_KeyMissingToGapOrSwitch = (o:{keyId: string}, x?: ObjectWithMetadata) => mx(
@ -165,8 +122,10 @@ export class LdmlCompilerMessages {
);
static ERROR_ExcessHardware = SevError | 0x0012;
static Error_ExcessHardware = (o:{formId: string}) => m(this.ERROR_ExcessHardware,
`layers formId=${def(o.formId)}: Can only have one non-'touch' element`);
static Error_ExcessHardware = (o:{formId: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_ExcessHardware, x,
`layers formId=${def(o.formId)}: Can only have one non-'touch' element`,
);
static ERROR_InvalidHardware = SevError | 0x0013;
static Error_InvalidHardware = (o: { formId: string }, x?: ObjectWithMetadata) => mx(
@ -174,18 +133,10 @@ export class LdmlCompilerMessages {
`layers has invalid value formId=${def(o.formId)}`,
);
private static layerIdOrEmpty(layer : string) {
if (layer) {
return ` on layer id=${def(layer)}`;
} else {
return '';
}
}
static ERROR_InvalidModifier = SevError | 0x0014;
static Error_InvalidModifier = (o:{modifiers: string, id: string}, x?: ObjectWithMetadata) => mx(
static Error_InvalidModifier = (o:{modifiers: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidModifier, x,
`layer has invalid modifiers='${def(o.modifiers)}'` + LdmlCompilerMessages.layerIdOrEmpty(o.id),
`layer has invalid modifiers='${def(o.modifiers)}'`,
);
static ERROR_MissingFlicks = SevError | 0x0015;
@ -202,20 +153,28 @@ export class LdmlCompilerMessages {
// Not hit due to XML parsing
static ERROR_InvalidTransformsType = SevError | 0x0018;
static Error_InvalidTransformsType = (o:{types: string[]}) =>
m(this.ERROR_InvalidTransformsType, `Invalid transforms types: '${def(o.types?.join(','))}'`);
static Error_InvalidTransformsType = (o:{type: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidTransformsType, x,
`Invalid transforms type: '${def(o.type)}'`,
);
static ERROR_DuplicateTransformsType = SevError | 0x0019;
static Error_DuplicateTransformsType = (o:{types: string[]}) =>
m(this.ERROR_DuplicateTransformsType, `Duplicate transforms types: '${def(o.types?.join(','))}'`);
static Error_DuplicateTransformsType = (o:{type: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DuplicateTransformsType, x,
`Duplicate transforms type: '${def(o.type)}'`,
);
static ERROR_MixedTransformGroup = SevError | 0x001A;
static Error_MixedTransformGroup = () =>
m(this.ERROR_MixedTransformGroup, `transformGroup cannot contain both reorder and transform elements`);
static Error_MixedTransformGroup = (x?: ObjectWithMetadata) => mx(
this.ERROR_MixedTransformGroup, x,
`transformGroup cannot contain both reorder and transform elements`,
);
static ERROR_EmptyTransformGroup = SevError | 0x001B;
static Error_EmptyTransformGroup = () =>
m(this.ERROR_EmptyTransformGroup, `transformGroup must have either reorder or transform elements`);
static Error_EmptyTransformGroup = (x?: ObjectWithMetadata) => mx(
this.ERROR_EmptyTransformGroup, x,
`transformGroup must have either reorder or transform elements`,
);
static ERROR_MissingStringVariable = SevError | 0x001C;
static Error_MissingStringVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
@ -224,12 +183,16 @@ export class LdmlCompilerMessages {
);
static ERROR_MissingSetVariable = SevError | 0x001D;
static Error_MissingSetVariable = (o:{id: string}) =>
m(this.ERROR_MissingSetVariable, `Reference to undefined set variable: \$[${def(o.id)}]`);
static Error_MissingSetVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingSetVariable, x,
`Reference to undefined set variable: \$[${def(o.id)}]`,
);
static ERROR_MissingUnicodeSetVariable = SevError | 0x001E;
static Error_MissingUnicodeSetVariable = (o:{id: string}) =>
m(this.ERROR_MissingUnicodeSetVariable, `Reference to undefined UnicodeSet variable: \$[${def(o.id)}]`);
static Error_MissingUnicodeSetVariable = (o:{id: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingUnicodeSetVariable, x,
`Reference to undefined UnicodeSet variable: \$[${def(o.id)}]`,
);
static ERROR_NeedSpacesBetweenSetVariables = SevError | 0x001F;
static Error_NeedSpacesBetweenSetVariables = (o:{item: string}, x?: ObjectWithMetadata) => mx(
@ -244,74 +207,107 @@ export class LdmlCompilerMessages {
);
static ERROR_MissingMarkers = SevError | 0x0021;
static Error_MissingMarkers = (o: { ids: string[] }) =>
m(this.ERROR_MissingMarkers, `Markers used for matching but not defined: ${def(o.ids?.join(','))}`);
static Error_MissingMarkers = (o: { ids: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_MissingMarkers, x,
`Markers used for matching but not defined: ${def(o.ids)}`
);
static ERROR_DisplayNeedsToOrId = SevError | 0x0022;
static Error_DisplayNeedsToOrId = (o:{output?: string, keyId?: string}) =>
m(this.ERROR_DisplayNeedsToOrId, `display ${LdmlCompilerMessages.outputOrKeyId(o)} needs output= or keyId=, but not both`);
static Error_DisplayNeedsToOrId = (o:{display?: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_DisplayNeedsToOrId, x,
`display display='${def(o.display)}' needs output= or keyId=, but not both`,
);
static HINT_PUACharacters = SevHint | 0x0023;
static Hint_PUACharacters = (o: { count: number, lowestCh: number }) =>
m(this.HINT_PUACharacters, `File contains ${def(o.count)} PUA character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static Hint_PUACharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.HINT_PUACharacters, x,
`File contains ${def(o.count)} PUA character(s), including ${def(o.lowestCh)}`,
);
static WARN_UnassignedCharacters = SevWarn | 0x0024;
static Warn_UnassignedCharacters = (o: { count: number, lowestCh: number }) =>
m(this.WARN_UnassignedCharacters, `File contains ${def(o.count)} unassigned character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static Warn_UnassignedCharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.WARN_UnassignedCharacters, x,
`File contains ${def(o.count)} unassigned character(s), including ${def(o.lowestCh)}`,
);
static ERROR_IllegalCharacters = SevError | 0x0025;
static Error_IllegalCharacters = (o: { count: number, lowestCh: number }) =>
m(this.ERROR_IllegalCharacters, `File contains ${def(o.count)} illegal character(s), including ${util.describeCodepoint(o.lowestCh)}`);
static Error_IllegalCharacters = (o: { count: number, lowestCh: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalCharacters, x,
`File contains ${def(o.count)} illegal character(s), including ${def(o.lowestCh) }`,
);
static HINT_CharClassImplicitDenorm = SevHint | 0x0026;
static Hint_CharClassImplicitDenorm = (o: { lowestCh: number }) =>
m(this.HINT_CharClassImplicitDenorm, `File has character classes which span non-NFD character(s), including ${util.describeCodepoint(o.lowestCh)}. These will not match any text.`);
static Hint_CharClassImplicitDenorm = (o: { lowestCh: number }, x?: ObjectWithMetadata) => mx(
this.HINT_CharClassImplicitDenorm, x,
`File has character classes which span non-NFD character(s), including ${def(o.lowestCh)}. These will not match any text.`,
);
static WARN_CharClassExplicitDenorm = SevWarn | 0x0027;
static Warn_CharClassExplicitDenorm = (o: { lowestCh: number }) =>
m(this.WARN_CharClassExplicitDenorm, `File has character classes which include non-NFD characters(s), including ${util.describeCodepoint(o.lowestCh)}. These will not match any text.`);
static Warn_CharClassExplicitDenorm = (o: { lowestCh: number }, x?: ObjectWithMetadata) => mx(
this.WARN_CharClassExplicitDenorm, x,
`File has character classes which include non-NFD characters(s), including ${def(o.lowestCh)}. These will not match any text.`,
);
static ERROR_UnparseableReorderSet = SevError | 0x0028;
static Error_UnparseableReorderSet = (o: { from: string, set: string }) =>
m(this.ERROR_UnparseableReorderSet, `Illegal UnicodeSet "${def(o.set)}" in reorder "${def(o.from)}`);
// Available: 0x0028
static ERROR_InvalidVariableIdentifier = SevError | 0x0029;
static Error_InvalidVariableIdentifier = (o: { id: string }) => m(
this.ERROR_InvalidVariableIdentifier,
static Error_InvalidVariableIdentifier = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidVariableIdentifier, x,
`Invalid variable identifier "${def(o.id)}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`,
);
static ERROR_InvalidMarkerIdentifier = SevError | 0x002A;
static Error_InvalidMarkerIdentifier = (o: { id: string }) => m(
this.ERROR_InvalidMarkerIdentifier,
static Error_InvalidMarkerIdentifier = (o: { id: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidMarkerIdentifier, x,
`Invalid marker identifier "\m{${def(o.id)}}". Identifiers must be between 1 and 32 characters, and can use A-Z, a-z, 0-9, and _.`,
);
static WARN_StringDenorm = SevWarn | 0x002B;
static Warn_StringDenorm = (o: { s: string }) =>
m(this.WARN_StringDenorm, `File contains string "${def(o.s)}" that is neither NFC nor NFD.`);
static Warn_StringDenorm = (o: { s: string }, x?: ObjectWithMetadata) => mx(
this.WARN_StringDenorm, x,
`File contains string "${def(o.s)}" that is neither NFC nor NFD.`,
);
// Available: 0x02C-0x2F
static ERROR_DuplicateLayerWidth = SevError | 0x002C;
static Error_DuplicateLayerWidth = (o: { minDeviceWidth: number }, x?: ObjectWithMetadata) => mx(
this.ERROR_DuplicateLayerWidth, x,
`Two or more layers have minDeviceWidth=${def(o.minDeviceWidth)}`,
`Touch layers must have distinct widths.`
);
static ERROR_InvalidLayerWidth = SevError | 0x002D;
static Error_InvalidLayerWidth = (o: { minDeviceWidth: number }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidLayerWidth, x,
`Invalid Layers minDeviceWidth=${def(o.minDeviceWidth)}`,
`Width must be between 1-999 (millimeters), inclusive.` // sync with layr_max_minDeviceWidth / layr_max_maxDeviceWidth (from spec)
);
// Available: 0x02E-0x2F
static ERROR_InvalidQuadEscape = SevError | 0x0030;
static Error_InvalidQuadEscape = (o: { cp: number }) =>
m(this.ERROR_InvalidQuadEscape, `Invalid escape "\\u${util.hexQuad(o?.cp || 0)}". Hint: Use "\\u{${def(o?.cp?.toString(16))}}"`);
static Error_InvalidQuadEscape = (o: { cp: string, recommended: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_InvalidQuadEscape, x,
`Invalid escape "${def(o.cp)}"`,
`**Hint**: Use "${def(o.recommended)}"`,
);
//
// Transform syntax errors begin at ...F00 (SevErrorTransform)
// This is a bit of a catch-all and represents messages bubbling up from the underlying regex engine
static ERROR_UnparseableTransformFrom = SevErrorTransform | 0x00;
static Error_UnparseableTransformFrom = (o: { from: string, message: string }) =>
m(this.ERROR_UnparseableTransformFrom, `Invalid transform from="${def(o.from)}": "${def(o.message)}"`);
static Error_UnparseableTransformFrom = (o: { from: string, message: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_UnparseableTransformFrom, x,
`Invalid transform from="${def(o.from)}": "${def(o.message)}"`,
);
//------------------------------------------------------------------------------|
// max length of detail message lines (checked by verifyCompilerMessagesObject) |
//------------------------------------------------------------------------------|
static ERROR_IllegalTransformDollarsign = SevErrorTransform | 0x01;
static Error_IllegalTransformDollarsign = (o: { from: string }) => m(
this.ERROR_IllegalTransformDollarsign,
static Error_IllegalTransformDollarsign = (o: { from: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformDollarsign, x,
`Invalid transform from="${def(o.from)}": Unescaped dollar-sign ($) is not valid transform syntax.`, `
**Hint**: Use \`\\$\` to match a literal dollar-sign. If this precedes a
variable name, the variable name may not be valid (A-Z, a-z, 0-9, _, 32
@ -319,57 +315,36 @@ export class LdmlCompilerMessages {
`);
static ERROR_TransformFromMatchesNothing = SevErrorTransform | 0x02;
static Error_TransformFromMatchesNothing = (o: { from: string }) => m(
this.ERROR_TransformFromMatchesNothing,
static Error_TransformFromMatchesNothing = (o: { from: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_TransformFromMatchesNothing, x,
`Invalid transfom from="${def(o.from)}": Matches an empty string.`
);
static ERROR_IllegalTransformPlus = SevErrorTransform | 0x03;
static Error_IllegalTransformPlus = (o: { from: string }) => m(
this.ERROR_IllegalTransformPlus,
static Error_IllegalTransformPlus = (o: { from: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformPlus, x,
`Invalid transform from="${def(o.from)}": Unescaped plus (+) is not valid transform syntax.`, `
**Hint**: Use \`\\+\` to match a literal plus.
`);
static ERROR_IllegalTransformAsterisk = SevErrorTransform | 0x04;
static Error_IllegalTransformAsterisk = (o: { from: string }) =>m(
this.ERROR_IllegalTransformAsterisk,
static Error_IllegalTransformAsterisk = (o: { from: string }, x?: ObjectWithMetadata) =>mx(
this.ERROR_IllegalTransformAsterisk, x,
`Invalid transform from="${def(o.from)}": Unescaped asterisk (*) is not valid transform syntax.`, `
**Hint**: Use \`\\*\` to match a literal asterisk.
`);
static ERROR_IllegalTransformToUset = SevErrorTransform | 0x05;
static Error_IllegalTransformToUset = (o: { to: string }) => m(
this.ERROR_IllegalTransformToUset,
static Error_IllegalTransformToUset = (o: { to: string }, x?: ObjectWithMetadata) => mx(
this.ERROR_IllegalTransformToUset, x,
`Invalid transform to="${def(o.to)}": Set variable (\\$[…]) cannot be used in 'to=' unless part of a map.`, `
**Hint**: If a map was meant, must use the form
\`<transform from="($[fromSet])" to="$[1:toSet]"/>\`.
`);
static ERROR_UnparseableTransformTo = SevErrorTransform | 0x06;
static Error_UnparseableTransformTo = (o: {to: string, message: string}) => m(
this.ERROR_UnparseableTransformTo,
static Error_UnparseableTransformTo = (o: {to: string, message: string}, x?: ObjectWithMetadata) => mx(
this.ERROR_UnparseableTransformTo, x,
`Invalid transform to="${def(o.to)}": "${def(o.message)}"`,
);
/**
* Get an offset from o and set e's offset field
* @param event a compiler event, such as from functions in this class
* @param x any object parsed from XML or with the XML_META_DATA_SYMBOL symbol copied over
* @returns modified event object
*/
static offset(event: CompilerEvent, x?: any): CompilerEvent {
if(x) {
const metadata = KeymanXMLReader.getMetaData(x) || {};
const offset = metadata?.startIndex;
if (offset) {
event.offset = offset;
}
const filename = event.filename || metadata[XML_FILENAME_SYMBOL];
if (filename) {
event.filename = filename;
}
}
return event;
}
}

View file

@ -35,13 +35,13 @@ export class LinterKeycaps extends Linter {
const disp = this.findDisp(id.value, to.value);
if (!disp) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForSwitch({ id: id.value }));
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForSwitch({ id: id.value }, key));
}
} else if (to.value !== '' && nonMarkerOutput === '') {
// has output, but only markers
const disp = this.findDisp(id.value, to.value);
if (!disp) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: id.value }));
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_NoDisplayForMarker({ id: id.value }, key));
}
}
}

View file

@ -1,6 +1,6 @@
import { constants } from "@keymanapp/ldml-keyboard-constants";
import { KMXPlus } from '@keymanapp/common-types';
import { LDMLKeyboard } from '@keymanapp/developer-utils';
import { LDMLKeyboard, ObjectWithMetadata } from '@keymanapp/developer-utils';
import { SectionCompiler } from "./section-compiler.js";
import { LdmlCompilerMessages } from "./ldml-compiler-messages.js";
@ -21,7 +21,17 @@ export class LocaCompiler extends SectionCompiler {
*/
private getLocales =
(keyboard: LKKeyboard) =>
[keyboard.locale].concat(Array.isArray(keyboard.locales?.locale) ? keyboard.locales.locale.map(v => v.id) : [])
[keyboard.locale].concat(Array.isArray(keyboard.locales?.locale) ? keyboard.locales.locale.map(v => v.id) : []);
/**
* Attempt to locate context metadata for a locale, for error messages
* @param loc locale tag
* @returns context metadata or null
*/
private contextForLocale(loc: string) : ObjectWithMetadata {
if (loc == this.keyboard3.locale) return this.keyboard3;
return this.keyboard3?.locales;
}
public validate(): boolean {
let valid = true;
@ -31,7 +41,7 @@ export class LocaCompiler extends SectionCompiler {
new Intl.Locale(tag);
} catch(e) {
if(e instanceof RangeError) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidLocale({tag}));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidLocale({tag}, this.contextForLocale(tag)));
valid = false;
} else {
/* c8 ignore next 2 */
@ -51,7 +61,8 @@ export class LocaCompiler extends SectionCompiler {
const locales = sourceLocales.map((sourceLocale: string) => {
const locale = new Intl.Locale(sourceLocale).minimize().toString();
if(locale != sourceLocale) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale, locale}));
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({ sourceLocale, locale },
this.contextForLocale(sourceLocale)));
}
return locale;
});
@ -60,10 +71,10 @@ export class LocaCompiler extends SectionCompiler {
// yet include `getCanonicalLocales` but node 16 does include it so we can
// safely use it. Also well supported in modern browsers.
const canonicalLocales = (Intl as any).getCanonicalLocales(locales) as string[];
result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale));
result.locales = canonicalLocales.map(locale => sections.strs.allocString(locale, {x: this.contextForLocale(locale)}));
if(result.locales.length < locales.length) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales());
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales(this.keyboard3?.locales));
}
return result;

View file

@ -53,12 +53,18 @@ export class MetaCompiler extends SectionCompiler {
public compile(sections: DependencySections): Meta {
const result = new Meta();
result.author = sections.strs.allocString(this.keyboard3.info?.author);
result.conform = sections.strs.allocString(this.keyboard3.conformsTo);
result.layout = sections.strs.allocString(this.keyboard3.info?.layout);
result.name = sections.strs.allocString(this.keyboard3.info?.name);
result.indicator = sections.strs.allocString(this.keyboard3.info?.indicator);
result.version = sections.strs.allocString(this.keyboard3.version?.number ?? "0.0.0");
result.author = sections.strs.allocString(this.keyboard3.info?.author,
{x: this.keyboard3.info});
result.conform = sections.strs.allocString(this.keyboard3.conformsTo,
{x: this.keyboard3});
result.layout = sections.strs.allocString(this.keyboard3.info?.layout,
{x: this.keyboard3.info});
result.name = sections.strs.allocString(this.keyboard3.info?.name,
{x: this.keyboard3.info});
result.indicator = sections.strs.allocString(this.keyboard3.info?.indicator,
{x: this.keyboard3.info});
result.version = sections.strs.allocString(this.keyboard3.version?.number ?? "0.0.0",
{x: this.keyboard3.version});
result.settings =
(this.keyboard3.settings?.normalization == "disabled" ? KeyboardSettings.normalizationDisabled : 0);
return result;

View file

@ -1,4 +1,5 @@
import { LdmlKeyboardTypes } from "@keymanapp/common-types";
import { ObjectWithMetadata } from "@keymanapp/developer-utils";
/**
* Verb for SubstitutionTracker.add()
@ -14,7 +15,7 @@ export enum SubstitutionUse {
variable,
}
type SubstitutionSet = Set<string>;
type SubstitutionSet = Map<string, ObjectWithMetadata>;
/** Tracks usage of markers */
export class SubstitutionTracker {
@ -28,10 +29,10 @@ export class SubstitutionTracker {
all: SubstitutionSet;
constructor() {
this.emitted = new Set<string>();
this.consumed = new Set<string>();
this.matched = new Set<string>();
this.all = new Set<string>();
this.emitted = new Map<string, ObjectWithMetadata>();
this.consumed = new Map<string, ObjectWithMetadata>();
this.matched = new Map<string, ObjectWithMetadata>();
this.all = new Map<string, ObjectWithMetadata>();
}
/**
@ -39,32 +40,33 @@ export class SubstitutionTracker {
* @param verb what kind of use we are adding
* @param markers list of substitutions to add
*/
add(verb: SubstitutionUse, markers: string[]) {
add(verb: SubstitutionUse, markers: string[], x?: ObjectWithMetadata) {
if (!markers.length) {
return; // skip if empty
}
x = x || {}; // need at least an empty object
if (verb == SubstitutionUse.emit) {
markers.forEach((m) => {
this.emitted.add(m);
this.all.add(m);
this.emitted.set(m, x);
this.all.set(m,x);
});
} else if (verb == SubstitutionUse.consume) {
markers.forEach((m) => {
this.consumed.add(m);
this.all.add(m);
this.consumed.set(m,x);
this.all.set(m,x);
});
} else if (verb == SubstitutionUse.match) {
markers.forEach((m) => {
this.matched.add(m);
this.all.add(m);
this.matched.set(m,x);
this.all.set(m,x);
});
} else if (verb == SubstitutionUse.variable) {
markers.forEach((m) => {
// we don't know, so add it to all three
this.matched.add(m);
this.emitted.add(m);
this.consumed.add(m);
this.all.add(m);
this.matched.set(m,x);
this.emitted.set(m,x);
this.consumed.set(m,x);
this.all.set(m,x);
});
/* c8 skip next 3 */
} else {
@ -75,27 +77,27 @@ export class SubstitutionTracker {
/** rollup of several substitution types */
export class Substitutions {
addSetAndStringSubtitution(verb: SubstitutionUse, str?: string) {
this.set.add(verb, LdmlKeyboardTypes.VariableParser.allSetReferences(str));
this.addStringAndMarkerSubstitution(verb, str);
addSetAndStringSubtitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.set.add(verb, LdmlKeyboardTypes.VariableParser.allSetReferences(str), x);
this.addStringAndMarkerSubstitution(verb, str, x);
}
/** add a string that can have string var substitutions or markers */
addStringAndMarkerSubstitution(verb: SubstitutionUse, str?: string) {
this.addMarkers(verb, str);
this.addStringSubstitution(verb, str);
addStringAndMarkerSubstitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.addMarkers(verb, str, x);
this.addStringSubstitution(verb, str, x);
}
addStringSubstitution(verb: SubstitutionUse, str?: string) {
this.string.add(verb, LdmlKeyboardTypes.VariableParser.allStringReferences(str));
addStringSubstitution(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.string.add(verb, LdmlKeyboardTypes.VariableParser.allStringReferences(str), x);
}
/** add a string that's just markers */
addMarkers(verb: SubstitutionUse, str?: string) {
this.markers.add(verb, LdmlKeyboardTypes.MarkerParser.allReferences(str));
LdmlKeyboardTypes.MarkerParser.allBrokenReferences(str).forEach(m => this.badMarkers.add(m));
addMarkers(verb: SubstitutionUse, str?: string, x?: ObjectWithMetadata) {
this.markers.add(verb, LdmlKeyboardTypes.MarkerParser.allReferences(str), x);
LdmlKeyboardTypes.MarkerParser.allBrokenReferences(str).forEach(m => this.badMarkers.set(m, x));
}
// all valid markers
markers: SubstitutionTracker;
// all invalid markers
badMarkers: Set<string>;
badMarkers: SubstitutionSet;
// all valid set ids
set: SubstitutionTracker;
// all valid string ids
@ -108,6 +110,6 @@ export class Substitutions {
this.set = new SubstitutionTracker();
this.string = new SubstitutionTracker();
this.uset = new SubstitutionTracker();
this.badMarkers = new Set<string>();
this.badMarkers = new Map<string, ObjectWithMetadata>();
}
}

View file

@ -1,6 +1,6 @@
import { constants, SectionIdent } from "@keymanapp/ldml-keyboard-constants";
import { KMXPlus, LdmlKeyboardTypes, util } from '@keymanapp/common-types';
import { CompilerCallbacks, LDMLKeyboard } from "@keymanapp/developer-utils";
import { CompilerCallbacks, LDMLKeyboard, ObjectWithMetadata } from "@keymanapp/developer-utils";
import { SectionCompiler } from "./section-compiler.js";
import Bksp = KMXPlus.Bksp;
@ -18,6 +18,7 @@ import { verifyValidAndUnique } from "../util/util.js";
import { LdmlCompilerMessages } from "./ldml-compiler-messages.js";
import { Substitutions, SubstitutionUse } from "./substitution-tracker.js";
import { transform_from_parse, transform_to_parse } from "../util/abnf/abnf.js";
import { StrsOptions } from "../../../../../common/web/types/src/kmx/kmx-plus/kmx-plus.js";
type TransformCompilerType = 'simple' | 'backspace';
@ -26,22 +27,24 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
static validateSubstitutions(keyboard: LDMLKeyboard.LKKeyboard, st: Substitutions): boolean {
keyboard?.transforms?.forEach(transforms =>
transforms.transformGroup.forEach(transformGroup => {
transformGroup.transform?.forEach(({ to, from }) => {
st.addSetAndStringSubtitution(SubstitutionUse.consume, from);
st.addSetAndStringSubtitution(SubstitutionUse.emit, to);
transformGroup.transform?.forEach((transform) => {
const { to, from } = transform;
st.addSetAndStringSubtitution(SubstitutionUse.consume, from, transform);
st.addSetAndStringSubtitution(SubstitutionUse.emit, to, transform);
const mapFrom = LdmlKeyboardTypes.VariableParser.CAPTURE_SET_REFERENCE.exec(from);
const mapTo = LdmlKeyboardTypes.VariableParser.MAPPED_SET_REFERENCE.exec(to || '');
if (mapFrom) {
// add the 'from' as a match
st.set.add(SubstitutionUse.consume, [mapFrom[1]]);
st.set.add(SubstitutionUse.consume, [mapFrom[1]], transform);
}
if (mapTo) {
// add the 'from' as a match
st.set.add(SubstitutionUse.emit, [mapTo[1]]);
st.set.add(SubstitutionUse.emit, [mapTo[1]], transform);
}
});
transformGroup.reorder?.forEach(({ before }) => {
st.addStringSubstitution(SubstitutionUse.consume, before);
transformGroup.reorder?.forEach((reorder) => {
const { before } = reorder;
st.addStringSubstitution(SubstitutionUse.consume, before, reorder);
});
}));
return true;
@ -55,37 +58,37 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
public validate(): boolean {
const reportMessage = this.callbacks.reportMessage.bind(this.callbacks);
const ALLOWED_TYPES = ["simple", "backspace"];
let valid = true;
const transforms = this?.keyboard3?.transforms;
if (transforms) {
const types : string[] = transforms.map(({type}) => type);
if (transforms && this.id == "tran") {
// Only run this part in the main "tran" compiler.
// we use this map to get back to a context object for the error message
const typeToObject = new Map<string, LKTransforms>();
transforms.forEach(t => typeToObject.set(t.type, t));
const types = transforms.map(({type}) => type);
if (!verifyValidAndUnique(types,
types => reportMessage(LdmlCompilerMessages.Error_DuplicateTransformsType({ types })),
new Set(['simple', 'backspace']),
types => reportMessage(LdmlCompilerMessages.Error_InvalidTransformsType({ types })))) {
types => types.forEach(type =>
reportMessage(LdmlCompilerMessages.Error_DuplicateTransformsType({ type }, typeToObject.get(type)))
),
new Set(ALLOWED_TYPES),
types => types.forEach(type =>
reportMessage(LdmlCompilerMessages.Error_InvalidTransformsType({ type }, typeToObject.get(type)))
)
)) {
valid = false;
}
// check for mixed groups
let mixed = false;
let empty = false;
transforms.forEach(({transformGroup}) => transformGroup.forEach((transformGroup) => {
transforms.forEach(({ type, transformGroup }) => transformGroup.forEach((transformGroup) => {
if (this.type != type) return; // only validate bskp in bksp, tran in tran, etc.
if (transformGroup.reorder?.length && transformGroup.transform?.length) {
mixed = true;
}
if (!transformGroup.reorder?.length && !transformGroup.transform?.length) {
empty = true;
valid = false;
reportMessage(LdmlCompilerMessages.Error_MixedTransformGroup(transformGroup));
} else if (!transformGroup.reorder?.length && !transformGroup.transform?.length) {
valid = false;
reportMessage(LdmlCompilerMessages.Error_EmptyTransformGroup(transformGroup));
}
}));
if (mixed) {
valid = false;
reportMessage(LdmlCompilerMessages.Error_MixedTransformGroup()); // report this once
}
if (empty) {
valid = false;
reportMessage(LdmlCompilerMessages.Error_EmptyTransformGroup()); // report this once
}
// TODO-LDML: linting here should check for identical from, but this involves a double-parse which is ugly
// TODO-LDML: unicodesets means that either we fully parse them and verify conflicting rules or the linting is imperfect
@ -96,7 +99,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
/* c8 ignore next 4 */
/** allocate a new TranBase subclass */
protected newTran(): TranBase {
throw Error(`Internal Error: newTran() not implemented`);
throw Error(`Internal Error: TranBase.newTran() not implemented (should not be called)`);
}
private compileTransforms(sections: DependencySections, transforms: LKTransforms): TranBase {
@ -138,6 +141,8 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
}
private compileTransform(sections: DependencySections, transform: LKTransform) : TranTransform {
// we have lots of strings to allocate, that will all have these options
const stropts : StrsOptions = { x: transform };
const result = new TranTransform();
// setup for serializing
result._from = transform.from;
@ -146,10 +151,10 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
// check for incorrect \uXXXX escapes. Do this before substituting markers or sets.
// We run this first because it's more helpful than the ABNF.
cookedFrom = this.checkEscapes(cookedFrom); // check for \uXXXX escapes before normalizing
cookedFrom = this.checkEscapes(cookedFrom, transform); // check for \uXXXX escapes before normalizing
if (cookedFrom === '') {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from: cookedFrom }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from: cookedFrom }, transform));
return null;
}
@ -157,11 +162,11 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
const mapFrom = LdmlKeyboardTypes.VariableParser.CAPTURE_SET_REFERENCE.exec(cookedFrom);
const mapTo = LdmlKeyboardTypes.VariableParser.MAPPED_SET_REFERENCE.exec(transform.to || '');
if (mapFrom && mapTo) { // TODO-LDML: error cases
result.mapFrom = sections.strs.allocString(mapFrom[1]); // var name
result.mapTo = sections.strs.allocString(mapTo[1]); // var name
result.mapFrom = sections.strs.allocString(mapFrom[1], stropts); // var name
result.mapTo = sections.strs.allocString(mapTo[1], stropts); // var name
} else {
result.mapFrom = sections.strs.allocString('');
result.mapTo = sections.strs.allocString('');
result.mapFrom = sections.strs.allocString('', stropts);
result.mapTo = sections.strs.allocString('', stropts);
// validate 'to' here
if (!this.isValidTo(transform.to || '')) {
@ -181,7 +186,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
cookedFrom = util.unescapeStringToRegex(cookedFrom);
// check for denormalized ranges
cookedFrom = this.checkRanges(cookedFrom); // check before normalizing
cookedFrom = this.checkRanges(cookedFrom, transform); // check before normalizing
if (!sections?.meta?.normalizationDisabled) {
// nfd here.
@ -199,21 +204,22 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
transform_from_parse(transform.from);
}
} catch (e) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from: cookedFrom, message: e.toString() }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from: cookedFrom, message: e.toString() }, transform));
return null;
}
// run the parser here next, on the original to
try {
transform_to_parse(transform.to || '');
} catch (e) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformTo({ to: transform.to || '', message: e.toString() }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformTo({ to: transform.to || '', message: e.toString() }, transform));
return null;
}
// cookedFrom is cooked above, since there's some special treatment
result.from = sections.strs.allocString(cookedFrom, {
unescape: false,
x: transform,
}, sections);
// 'to' is handled via allocString
result.to = sections.strs.allocString(transform.to, {
@ -221,6 +227,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
markers: true,
unescape: true,
nfd: true,
x: transform,
}, sections);
return result;
}
@ -230,9 +237,9 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* We have already checked that it's not a mapTo,
* so there should not be any illegal substitutions.
*/
private isValidTo(to: string) : boolean {
private isValidTo(to: string, x?: ObjectWithMetadata) : boolean {
if (/(?<!\\)(?:\\\\)*\$\[/.test(to)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformToUset({ to }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformToUset({ to }, x));
return false;
}
return true;
@ -244,18 +251,18 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* @param from the original from - for error reporting
* @returns true if OK
*/
private isValidRegex(cookedFrom: string, from: string) : boolean {
private isValidRegex(cookedFrom: string, from: string, x?: ObjectWithMetadata) : boolean {
// check for any unescaped dollar sign here
if (/(?<!\\)(?:\\\\)*\$/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformDollarsign({ from }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformDollarsign({ from }, x));
return false;
}
if (/(?<!\\)(?:\\\\)*\*/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformAsterisk({ from }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformAsterisk({ from }, x));
return false;
}
if (/(?<!\\)(?:\\\\)*\+/.test(cookedFrom)) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformPlus({ from }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_IllegalTransformPlus({ from }, x));
return false;
}
// Verify that the regex is syntactically valid
@ -265,14 +272,14 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
// does it match an empty string?
if (rg.test('')) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_TransformFromMatchesNothing({ from }, x));
return false;
}
} catch (e) {
// We're exposing the internal regex error message here.
// In the future, CLDR plans to expose the EBNF for the transform,
// at which point we would have more precise validation prior to getting to this point.
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from, message: e.message }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_UnparseableTransformFrom({ from, message: e.message }, x));
return false;
}
return true;
@ -294,14 +301,14 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
result._before = reorder.before;
result._from = reorder.from;
result._order = reorder.order;
if (reorder.from && this.checkEscapes(reorder.from) === null) {
if (reorder.from && this.checkEscapes(reorder.from, reorder) === null) {
return null; // error'ed
}
if (reorder.before && this.checkEscapes(reorder.before) === null) {
if (reorder.before && this.checkEscapes(reorder.before, reorder) === null) {
return null; // error'ed
}
result.elements = sections.elem.allocElementString(sections, reorder.from, reorder.order, reorder.tertiary, reorder.tertiaryBase, reorder.preBase);
result.before = sections.elem.allocElementString(sections, reorder.before);
result.elements = sections.elem.allocElementString(sections, {x:reorder}, reorder.from, reorder.order, reorder.tertiary, reorder.tertiaryBase, reorder.preBase);
result.before = sections.elem.allocElementString(sections, {x:reorder}, reorder.before);
if (!result.elements || !result.before) {
return null; // already error'ed
} else {
@ -337,16 +344,21 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* @param cookedFrom the original string
* @returns the original string, or null if an error was reported
*/
private checkEscapes(cookedFrom: string): string | null {
private checkEscapes(cookedFrom: string, transform?: ObjectWithMetadata): string | null {
if (!cookedFrom) return cookedFrom;
// should not follow marker prefix, nor marker prefix with range
const anyQuad = /(?<!\\uffff\\u0008(?:\[[0-9a-fA-F\\u-]*)?)\\u([0-9a-fA-F]{4})/g;
for (const [, sub] of cookedFrom.matchAll(anyQuad)) {
for (const [matched, sub] of cookedFrom.matchAll(anyQuad)) {
const s = util.unescapeOne(sub);
if (s !== '\uffff' && s !== '\u0008') { // markers
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: s.codePointAt(0) }));
const codepoint: number = s.codePointAt(0);
const cp: string = matched; // the original match from the file
const recommended: string = `\\u{${codepoint.toString(16)}}`;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidQuadEscape({
cp, recommended
}, transform));
return null; // exit on the first error
}
}
@ -359,7 +371,7 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
* @param cookedFrom input regex string
* @returns updated 'from' string
*/
private checkRanges(cookedFrom: string): string {
private checkRanges(cookedFrom: string, transform?: LKTransform): string {
if (!cookedFrom) return cookedFrom;
// extract all of the potential ranges - but don't match any-markers!
@ -408,13 +420,17 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
let needCooking = false;
const explicitSet = rangeExplicit.analyze()?.get(util.BadStringType.denormalized);
if (explicitSet) {
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_CharClassExplicitDenorm({ lowestCh: explicitSet.values().next().value }));
this.callbacks.reportMessage(LdmlCompilerMessages.Warn_CharClassExplicitDenorm({
lowestCh: explicitSet.values().next().value
}, transform));
needCooking = true;
} else {
// don't analyze the implicit set of THIS range, if explicit is already problematic
const implicitSet = rangeImplicit.analyze()?.get(util.BadStringType.denormalized);
if (implicitSet) {
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_CharClassImplicitDenorm({ lowestCh: implicitSet.values().next().value }));
this.callbacks.reportMessage(LdmlCompilerMessages.Hint_CharClassImplicitDenorm({
lowestCh: implicitSet.values().next().value
}, transform));
needCooking = true;
}
}
@ -472,4 +488,8 @@ export class BkspCompiler extends TransformCompiler<'backspace', Bksp /*, BkspIt
public get id() {
return constants.section.bksp;
}
/** */
public validate(): boolean {
return true;
}
};

View file

@ -1,6 +1,6 @@
import { SectionIdent, constants } from "@keymanapp/ldml-keyboard-constants";
import { KMXPlus, LdmlKeyboardTypes } from '@keymanapp/common-types';
import { LDMLKeyboard, CompilerCallbacks } from '@keymanapp/developer-utils';
import { LDMLKeyboard, CompilerCallbacks, ObjectWithMetadata } from '@keymanapp/developer-utils';
import { SectionCompiler } from "./section-compiler.js";
import Vars = KMXPlus.Vars;
import StringVarItem = KMXPlus.StringVarItem;
@ -40,9 +40,9 @@ export class VarsCompiler extends SectionCompiler {
return valid;
}
private validateIdentifier(id: string) {
private validateIdentifier(id: string, x?: ObjectWithMetadata) {
if(!id.match(LdmlKeyboardTypes.VariableParser.ID)) { // From <string> DTD
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifier({id}));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidVariableIdentifier({id}, x));
return false;
}
return true;
@ -77,7 +77,7 @@ export class VarsCompiler extends SectionCompiler {
// Strings
for (const e of variables.string) {
const { id, value } = e;
if(!this.validateIdentifier(id)) {
if(!this.validateIdentifier(id, e)) {
valid = false;
continue;
}
@ -90,9 +90,9 @@ export class VarsCompiler extends SectionCompiler {
allStrings.add(ref); // avoids multiple reports of same missing variable
}
}
st.string.add(SubstitutionUse.variable, stringrefs);
st.string.add(SubstitutionUse.variable, stringrefs, e);
allStrings.add(id);
st.addMarkers(SubstitutionUse.variable, value);
st.addMarkers(SubstitutionUse.variable, value, e);
}
// Sets
for (const e of variables.set) {
@ -105,7 +105,7 @@ export class VarsCompiler extends SectionCompiler {
allSets.add(id);
// check for illegal references, here.
const stringrefs = LdmlKeyboardTypes.VariableParser.allStringReferences(value);
st.string.add(SubstitutionUse.variable, stringrefs);
st.string.add(SubstitutionUse.variable, stringrefs, e);
// Now split into spaces.
const items: string[] = LdmlKeyboardTypes.VariableParser.setSplitter(value);
@ -116,7 +116,7 @@ export class VarsCompiler extends SectionCompiler {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_NeedSpacesBetweenSetVariables({ item }, e));
} else {
st.set.add(SubstitutionUse.variable, setrefs); // the reference to a 'map'
st.set.add(SubstitutionUse.variable, setrefs, e); // the reference to a 'map'
}
// TODO-LDML: Are there other illegal cases here? what about "x$[set]"?
}
@ -131,7 +131,7 @@ export class VarsCompiler extends SectionCompiler {
addId(id, e);
allUnicodeSets.add(id);
const stringrefs = LdmlKeyboardTypes.VariableParser.allStringReferences(value);
st.string.add(SubstitutionUse.variable, stringrefs);
st.string.add(SubstitutionUse.variable, stringrefs, e);
const setrefs = LdmlKeyboardTypes.VariableParser.allSetReferences(value);
for (const id2 of setrefs) {
if (!allUnicodeSets.has(id2)) {
@ -140,7 +140,7 @@ export class VarsCompiler extends SectionCompiler {
// $[set] in a UnicodeSet must be another UnicodeSet.
this.callbacks.reportMessage(LdmlCompilerMessages.Error_CantReferenceSetFromUnicodeSet({ id: id2 }, e));
} else {
st.uset.add(SubstitutionUse.variable, [id2]);
st.uset.add(SubstitutionUse.variable, [id2], e);
}
}
}
@ -157,24 +157,24 @@ export class VarsCompiler extends SectionCompiler {
}
// check for any missing vars
for (const id of st.set.all) {
for (const [id,x] of st.set.all.entries()) {
// note: we check the uset list also. we don't know until later which was
// intended. collisions are handled separately.
if (!allSets.has(id) && !allUnicodeSets.has(id)) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingSetVariable({ id }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingSetVariable({ id }, x));
}
}
for (const id of st.string.all) {
for (const [id,x] of st.string.all.entries()) {
if (!allStrings.has(id)) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({ id }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingStringVariable({ id }, x));
}
}
for (const id of st.uset.all) {
for (const [id,x] of st.uset.all.entries()) {
if (!allUnicodeSets.has(id)) {
valid = false;
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingUnicodeSetVariable({ id }));
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingUnicodeSetVariable({ id }, x));
}
}
@ -198,13 +198,13 @@ export class VarsCompiler extends SectionCompiler {
// see if there are any matched-but-not-emitted markers
const matchedNotEmitted : Set<string> = new Set<string>();
const mt = st.markers;
for (const m of mt.matched.values()) {
for (const m of mt.matched.keys()) {
if (m === LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID) continue; // match-all marker
if (!mt.emitted.has(m)) {
matchedNotEmitted.add(m);
}
}
for (const m of mt.consumed.values()) {
for (const m of mt.consumed.keys()) {
if (m === LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID) continue; // match-all marker
if (!mt.emitted.has(m)) {
matchedNotEmitted.add(m);
@ -213,7 +213,9 @@ export class VarsCompiler extends SectionCompiler {
// report once
if (matchedNotEmitted.size > 0) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingMarkers({ ids: Array.from(matchedNotEmitted.values()).sort() }));
const ids = Array.from(matchedNotEmitted.values()).sort();
const x = mt.all.get(ids[0]); // get the FIRST object for context
this.callbacks.reportMessage(LdmlCompilerMessages.Error_MissingMarkers({ ids: ids.join(',')}, x));
valid = false;
}
@ -222,19 +224,24 @@ export class VarsCompiler extends SectionCompiler {
if (!!st.badMarkers.size) {
valid = false;
st.badMarkers.forEach(id =>
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidMarkerIdentifier({ id })));
for (const [id, x] of st.badMarkers.entries()) {
this.callbacks.reportMessage(LdmlCompilerMessages.Error_InvalidMarkerIdentifier({ id }, x));
}
}
return valid;
}
validateSubstitutions(keyboard: LDMLKeyboard.LKKeyboard, st : Substitutions) : boolean {
keyboard?.variables?.string?.forEach(({value}) =>
st.markers.add(SubstitutionUse.variable, LdmlKeyboardTypes.MarkerParser.allReferences(value)));
keyboard?.variables?.string?.forEach((string) => {
const {value} = string;
st.markers.add(SubstitutionUse.variable, LdmlKeyboardTypes.MarkerParser.allReferences(value), string);
});
// get markers mentioned in a set
keyboard?.variables?.set?.forEach(({ value }) =>
LdmlKeyboardTypes.VariableParser.setSplitter(value).forEach(v => st.markers.add(SubstitutionUse.match, LdmlKeyboardTypes.MarkerParser.allReferences(v))));
keyboard?.variables?.set?.forEach((set) => {
const { value } = set;
LdmlKeyboardTypes.VariableParser.setSplitter(value).forEach(v => st.markers.add(SubstitutionUse.match, LdmlKeyboardTypes.MarkerParser.allReferences(v), set));
});
return true;
}
@ -260,8 +267,16 @@ export class VarsCompiler extends SectionCompiler {
const mt = st.markers;
// collect all markers, excluding the match-all
const allMarkers : string[] = Array.from(mt.all).filter(m => m !== LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID).sort();
result.markers = sections.list.allocList(allMarkers, {}, sections);
const allMarkers : string[] = Array.from(mt.all.keys()).filter(m => m !== LdmlKeyboardTypes.MarkerParser.ANY_MARKER_ID).sort();
result.markers = sections.list.allocList(allMarkers, {
// pass the first object in the listr
x: mt.all.get(allMarkers[0]||'')
// however, this is the string with the marker *id*. So, from the StrsCompiler
// point of view, the value of this context would be if the marker ID had, say,
// invalid Unicode in it for ERROR_IllegalCharacters - but, that wouldn't be a
// valid Marker ID either.
// So it's not likely that this context will be used, but it's available.
}, sections);
// sets need to be added late, because they can refer to markers
variables?.set?.forEach((e) =>

View file

@ -120,15 +120,15 @@ export function allUsedKeyIdsInFlick(flick? : LDMLKeyboard.LKFlick) : Set<string
* @param onInvalid callback with array of invalid values, deduped
* @returns true if all OK
*/
export function verifyValidAndUnique(
values: string[],
onDuplicate: (duplicates: string[]) => void,
allowed?: Set<string>,
onInvalid?: (invalids: string[]) => void)
export function verifyValidAndUnique<T>(
values: T[],
onDuplicate: (duplicates: T[]) => void,
allowed?: Set<T>,
onInvalid?: (invalids: T[]) => void)
: boolean {
const dups: string[] = [];
const invalids: string[] = [];
const seen = new Set<string>();
const dups: T[] = [];
const invalids: T[] = [];
const seen = new Set<T>();
for (const value of values) {
if (allowed && !allowed.has(value)) {
invalids.push(value);
@ -140,7 +140,7 @@ export function verifyValidAndUnique(
}
}
function dedupedSortedArray(values: string[]) : string[] {
function dedupedSortedArray(values: T[]) : T[] {
return Array.from(new Set(values)).sort();
}

View file

@ -1,11 +1,13 @@
import 'mocha';
import {assert} from 'chai';
import hextobin from '@keymanapp/hextobin';
import {compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture} from './helpers/index.js';
import {compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture, scrubContextFromMessages} from './helpers/index.js';
import { compareXml } from './helpers/compareXml.js';
import { LdmlKeyboardCompiler } from '../src/compiler/compiler.js';
import { kmxToXml } from '../src/util/serialize.js';
import { writeFileSync } from 'node:fs';
import { LdmlCompilerMessages } from '../src/main.js';
import { util } from '@keymanapp/common-types';
/** Overall compiler tests */
describe('compiler-tests', function() {
@ -42,6 +44,25 @@ describe('compiler-tests', function() {
assert.isNotNull(kvk?.data);
});
it('should-validate-on-run compiling sections/strs/invalid-illegal.xml', async function() {
this.timeout(4000);
const inputFilename = makePathToFixture('sections/strs/invalid-illegal.xml');
// should fail validation
const k = new LdmlKeyboardCompiler();
await k.init(compilerTestCallbacks, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false });
const runOutput = await k.run(inputFilename, "invalid-illegal.kmx"); // need the exact name passed to build-fixtures
assert.isNull(runOutput, "Expect invalid-illegal to fail to run()");
assert.sameDeepMembers( scrubContextFromMessages(compilerTestCallbacks.messages), [
// copied from strs.tests.ts
// validation messages
LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: util.describeCodepoint(0xFDD0) }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
]);
});
it('should-serialize-kmx', async function() {
this.timeout(4000);
// Let's build basic.xml

View file

@ -1,7 +1,7 @@
import 'mocha';
import {assert} from 'chai';
import { DispCompiler } from '../src/compiler/disp.js';
import { compilerTestCallbacks, loadSectionFixture, testCompilationCases } from './helpers/index.js';
import { compilerTestCallbacks, loadSectionFixture, testCompilationCases, withOffset } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
@ -67,25 +67,25 @@ describe('disp', function () {
const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupto.xml', compilerTestCallbacks) as Disp;
assert.isNull(disp);
assert.equal(compilerTestCallbacks.messages.length, 1);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ output: 'e' }));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ display: '(e)' }, withOffset(330)));
});
it('should reject duplicate ids', async function() {
const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-dupid.xml', compilerTestCallbacks) as Disp;1
assert.isNull(disp);
assert.equal(compilerTestCallbacks.messages.length, 1);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ keyId: 'e' }));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayIsRepeated({ display: '(e)' }, withOffset(329)));
});
it('should reject if neither to nor id', async function() {
const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-none.xml', compilerTestCallbacks) as Disp;
assert.isNull(disp);
assert.equal(compilerTestCallbacks.messages.length, 1);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({}));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: '(f)'}, withOffset(182)));
});
it('should reject if both to and id', async function() {
const disp = await loadSectionFixture(DispCompiler, 'sections/disp/invalid-both.xml', compilerTestCallbacks) as Disp;
assert.isNull(disp);
assert.equal(compilerTestCallbacks.messages.length, 1);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ output: 'e', keyId: 'e' }));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_DisplayNeedsToOrId({ display: '(e)' }, withOffset(182)));
});
testCompilationCases(DispCompiler, [
{

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="layr-error-custom-form"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="120">
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="0"> <!-- dup -->
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="layr-error-custom-form"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="6">
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="1024"> <!-- dup -->
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="layr-error-custom-form"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="1500">
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,22 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="layr-error-custom-form"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="x">
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -0,0 +1,29 @@
<?xml version="1.0" encoding="UTF-8"?>
<keyboard3 xmlns="https://schemas.unicode.org/cldr/45/keyboard3" locale="und" conformsTo="45">
<info name="layr-error-custom-form"/>
<keys>
<key id="one" output="1" />
<key id="two" output="2" />
<key id="three" output="3" />
</keys>
<layers formId="iso">
<layer id="base">
<row keys="one two three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="120">
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
<layers formId="touch" minDeviceWidth="120"> <!-- dup -->
<layer id="base">
<row keys="one" />
<row keys="two" />
<row keys="three" />
</layer>
</layers>
</keyboard3>

View file

@ -5,4 +5,5 @@
<keys />
<!-- invalid: need a layer! -->
<!-- this keyboard doesn't even have a <layers/> element - no offset will be generated -->
</keyboard3>

View file

@ -144,7 +144,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil
const valid = await k.validate(source);
if (validateMessages) {
assert.sameDeepMembers(compilerTestCallbacks.messages, validateMessages, "validation messages mismatch");
assert.sameDeepMembers(scrubContextFromMessages(compilerTestCallbacks.messages), validateMessages, "validation messages mismatch");
assert.notEqual(valid, expectFailValidate, 'validation failure');
} else {
checkMessages();
@ -155,7 +155,7 @@ export async function compileKeyboard(inputFilename: string, options: LdmlCompil
const kmx = await k.compile(source);
if (compileMessages) {
assert.sameDeepMembers(compilerTestCallbacks.messages, compileMessages, "compiler messages mismatch");
assert.sameDeepMembers(scrubContextFromMessages(compilerTestCallbacks.messages), compileMessages, "compiler messages mismatch");
} else {
checkMessages();
}
@ -259,6 +259,26 @@ export interface CompilationCase {
retainOffsetInMessages?: boolean;
}
/**
* Scrub 'context' from messages. to simplify unit tests
* @param messages input array of messages
* @returns copy of messages
*/
export function scrubContextFromMessages(messages: CompilerEvent[]): CompilerEvent[] {
return messages.map(m => {
const scrubbed = Object.assign({}, m);
// Turn this on once all messages have offsets, see messages.tests.ts
// if (!scrubbed.offset) {
// throw Error(`Error, no offset detected in message ${CompilerError.formatEvent(m)}`);
// }
delete scrubbed.offset;
delete scrubbed.line;
delete scrubbed.filename;
delete scrubbed.column;
return scrubbed;
});
}
/**
* Run a bunch of cases
* @param cases cases to run
@ -283,15 +303,7 @@ export function testCompilationCases(compiler: SectionCompilerNew, cases : Compi
let messagesToCheck = callbacks.messages;
// scrub offsets from messages to reduce churn in the test casws
if (!testcase.retainOffsetInMessages && callbacks.messages) {
messagesToCheck = callbacks.messages.map(m => {
const scrubbed = Object.assign({}, m);
// Turn this on once all messages have offsets, see messages.tests.ts
// if (!scrubbed.offset) {
// throw Error(`Error, no offset detected in message ${CompilerError.formatEvent(m)}`);
// }
delete scrubbed.offset;
return scrubbed;
});
messagesToCheck = scrubContextFromMessages(callbacks.messages);
}
const testcaseErrors = matchCompilerEventsOrBoolean(messagesToCheck, testcase.errors);
const testcaseWarnings = matchCompilerEvents(messagesToCheck, testcase.warnings);

View file

@ -312,7 +312,7 @@ describe('keys.kmap', function () {
{
subpath: 'sections/keys/invalid-bad-modifier.xml',
errors: [
LdmlCompilerMessages.Error_InvalidModifier({id:'base',modifiers:'altR-shift'}),
LdmlCompilerMessages.Error_InvalidModifier({ modifiers: 'altR-shift' }),
]
},
{

View file

@ -2,7 +2,7 @@ import 'mocha';
import { assert } from 'chai';
import { LayrCompiler } from '../src/compiler/layr.js';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { compilerTestCallbacks, testCompilationCases } from './helpers/index.js';
import { compilerTestCallbacks, testCompilationCases, withOffset } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { constants } from '@keymanapp/ldml-keyboard-constants';
@ -96,8 +96,7 @@ describe('layr', function () {
subpath: 'sections/keys/invalid-bad-modifier.xml',
errors: [
LdmlCompilerMessages.Error_InvalidModifier({
id: 'base',
modifiers: 'altR-shift'
modifiers: 'altR-shift',
}),
],
},
@ -108,7 +107,14 @@ describe('layr', function () {
{
// missing layer element
subpath: 'sections/layr/invalid-missing-layer.xml',
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement(withOffset(258))],
retainOffsetInMessages: true,
},
{
// missing layer element
subpath: 'sections/layr/invalid-missing-layer2.xml',
errors: [LdmlCompilerMessages.Error_MustBeAtLeastOneLayerElement()],
retainOffsetInMessages: true,
},
{
// keep in sync with similar test in test-keys.ts
@ -132,7 +138,7 @@ describe('layr', function () {
{
subpath: 'sections/layr/error-bogus-modifiers.xml',
errors: [
LdmlCompilerMessages.Error_InvalidModifier({ id: '', modifiers: 'caps bogus'}),
LdmlCompilerMessages.Error_InvalidModifier({ modifiers: 'caps bogus' }),
]
},
{
@ -160,5 +166,18 @@ describe('layr', function () {
}
},
},
{
subpath: 'sections/layr/error-dup-width.xml',
errors: [
LdmlCompilerMessages.Error_DuplicateLayerWidth({ minDeviceWidth: 120}),
]
},
...[0, 1024, 1500, `x` as unknown as number].map(minDeviceWidth => ({
subpath: `sections/layr/error-bad-width-${minDeviceWidth}.xml`,
errors: [
//
LdmlCompilerMessages.Error_InvalidLayerWidth({ minDeviceWidth }),
]
})),
]);
});

View file

@ -1,7 +1,7 @@
import 'mocha';
import { assert } from 'chai';
import { LocaCompiler } from '../src/compiler/loca.js';
import { compilerTestCallbacks, loadSectionFixture } from './helpers/index.js';
import { compilerTestCallbacks, loadSectionFixture, withOffset } from './helpers/index.js';
import { KMXPlus } from '@keymanapp/common-types';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
@ -26,10 +26,10 @@ describe('loca', function () {
// Note: multiple.xml includes fr-FR twice, with differing case, which should be canonicalized
assert.equal(compilerTestCallbacks.messages.length, 4);
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-FR', locale: 'fr'}));
assert.deepEqual(compilerTestCallbacks.messages[1], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'km-khmr-kh', locale: 'km'}));
assert.deepEqual(compilerTestCallbacks.messages[2], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-fr', locale: 'fr'}));
assert.deepEqual(compilerTestCallbacks.messages[3], LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales());
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-FR', locale: 'fr'}, withOffset(136)));
assert.deepEqual(compilerTestCallbacks.messages[1], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'km-khmr-kh', locale: 'km'}, withOffset(136)));
assert.deepEqual(compilerTestCallbacks.messages[2], LdmlCompilerMessages.Hint_LocaleIsNotMinimalAndClean({sourceLocale: 'fr-fr', locale: 'fr'}, withOffset(136)));
assert.deepEqual(compilerTestCallbacks.messages[3], LdmlCompilerMessages.Hint_OneOrMoreRepeatedLocales(withOffset(136)));
// Original is 6 locales, now five minimized in the results
assert.equal(loca.locales.length, 5);
@ -46,6 +46,6 @@ describe('loca', function () {
assert.equal(compilerTestCallbacks.messages.length, 1);
// We'll only test one invalid BCP 47 tag to verify that we are properly calling BCP 47 validation routines.
// Furthermore, we are testing BCP 47 structure, not the validity of each subtag -- we must assume the author knows of new subtags!
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidLocale({tag:'en-*'}));
assert.deepEqual(compilerTestCallbacks.messages[0], LdmlCompilerMessages.Error_InvalidLocale({tag:'en-*'}, withOffset(40)));
})
});

View file

@ -15,18 +15,20 @@ describe('LdmlCompilerMessages', function () {
/** all fns */
let total = 0;
/** does not take line numbers */
let noLines = 0;
const noLines = new Set<string>();
/** takes line numbers */
let lines = 0;
const fakeOffsetNumber = 1234;
const fakeOffsetObject = withOffset(fakeOffsetNumber);
for(const key of keys) {
if(typeof m[key] == 'function') {
// exclude this one, does not need line numbers
if (key == 'Error_InvalidFile') continue;
if (typeof m[key] == 'function') {
total++;
const f = m[key] as Function;
// console.log(`${f.name}: ${f.length}`);
if (f.length === 0) { // Error_foo()
noLines++;
noLines.add(key);
continue;
}
// now try to call it
@ -42,16 +44,30 @@ describe('LdmlCompilerMessages', function () {
expect(resp.offset).to.equal(fakeOffsetNumber, `Offset number round trip for error ${f.name} did not work, check the message function`);
} else {
// did not get a column number back
noLines++;
noLines.add(key);
}
}
}
expect(lines).to.not.be.equal(0, `None of ${total} messages had offset reporting.`);
if (noLines > 0) {
// Once this goes to zero, make it an error if it goes up!
// Oh, and while you're here, once this is zero, uncomment the code in testCompilationCases
// that asserts that all messages are actually generated with an offset.
console.warn(`TODO-LDML (#10622) ${noLines}/${total} messages did not have detectable offset (line number) reporting.`);
expect(Array.from(noLines.values())).to.deep.equal([], `${noLines.size}/${total} ${Number((noLines.size/total)*100).toFixed(0)}% of message(s) did not have detectable offset (line number) reporting`);
});
it('Should only have simple ${def(o...)} interpolation', () => {
const m = LdmlCompilerMessages as Record<string,any>;
const keys = Object.keys(LdmlCompilerMessages);
const complexInterpolation : string[] = [];
for(const key of keys) {
// exclude this one, does not need line numbers
if (key == 'Error_InvalidFile') continue;
if (typeof m[key] == 'function') {
const f = m[key] as Function;
const s = f.toString();
// scan for interpolation
// Interpolation.. A non-escaped ${ that's NOT followed by `def(o.` triggers this error.
if (/.*`.*(?<!\\)(?:\\\\)*\$\{(?!def\(o\.)/.test(s)) {
complexInterpolation.push(key);
}
}
}
expect(complexInterpolation).to.deep.equal([], 'Complex interpolation found, please only use the ${def(o... form')
});
});

View file

@ -2,6 +2,7 @@ import 'mocha';
import { assert } from 'chai';
import { compileKeyboard, compilerTestCallbacks, compilerTestOptions, makePathToFixture } from './helpers/index.js';
import { LdmlCompilerMessages } from '../src/compiler/ldml-compiler-messages.js';
import { util } from '@keymanapp/common-types';
/** strs tests */
@ -17,8 +18,8 @@ describe('strs', function () {
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: 0xFDD0 }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Error_IllegalCharacters({ count: 5, lowestCh: util.describeCodepoint(0xFDD0) }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
],
true, // validation should fail
[
@ -32,12 +33,12 @@ describe('strs', function () {
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
],
false, // validation should pass
[
// same messages
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
]);
assert.isNotNull(kmx);
});
@ -47,14 +48,14 @@ describe('strs', function () {
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
// validation messages
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: util.describeCodepoint(0x0CFFFD) }),
],
false, // validation should pass
[
// same messages
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: 0xE010 }),
LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: 0x0CFFFD }),
LdmlCompilerMessages.Hint_PUACharacters({ count: 2, lowestCh: util.describeCodepoint(0xE010) }),
LdmlCompilerMessages.Warn_UnassignedCharacters({ count: 1, lowestCh: util.describeCodepoint(0x0CFFFD) }),
]);
assert.isNotNull(kmx);
});
@ -63,7 +64,8 @@ describe('strs', function () {
const inputFilename = makePathToFixture('sections/tran/fail-bad-tran-2.xml');
const kmx = await compileKeyboard(inputFilename, { ...compilerTestOptions, saveDebug: true, shouldAddCompilerVersion: false },
[
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 295 }),
// \uXXXX is not allowed in this context
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: "\\u0127", recommended: "\\u{127}" }),
],
true, // validation should fail
[

View file

@ -129,7 +129,7 @@ describe('tran', function () {
{
subpath: 'sections/tran/fail-duplicate-type.xml',
errors: [
LdmlCompilerMessages.Error_DuplicateTransformsType({types: ['simple']})
LdmlCompilerMessages.Error_DuplicateTransformsType({type: 'simple'})
]
},
{
@ -273,13 +273,14 @@ describe('tran', function () {
{
subpath: 'sections/tran/fail-bad-reorder-2.xml',
errors: [
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 0x1a6b }),
// 'cp' has exact casing from original
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: "\\u1A6B", recommended: "\\u{1a6b}" }),
],
},
{
subpath: 'sections/tran/fail-bad-reorder-3.xml',
errors: [
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 0x1a60 }),
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: "\\u1A60", recommended: "\\u{1a60}" }),
],
},
// error due to bad regex
@ -295,7 +296,7 @@ describe('tran', function () {
// also used in test-compiler-e2e.ts
subpath: `sections/tran/fail-bad-tran-2.xml`,
errors: [
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: 295 }),
LdmlCompilerMessages.Error_InvalidQuadEscape({ cp: "\\u0127", recommended: "\\u{127}" }),
],
},
{
@ -466,7 +467,7 @@ describe('bksp', function () {
'doesnt_exist_1',
'doesnt_exist_2',
'doesnt_exist_3',
]
].join(',')
}),
],
},

View file

@ -238,7 +238,7 @@ describe('vars', function () {
'doesnt_exist_1',
'doesnt_exist_2',
'doesnt_exist_3',
]
].join(',')
}),
],
},

View file

@ -6,6 +6,7 @@
"url": "https://github.com/keymanapp/keyman/issues"
},
"homepage": "https://keyman.com/",
"scripts": {},
"dependencies": {
"monaco-editor": "0.15.6"
}

View file

@ -15,6 +15,8 @@ THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/build-utils.sh"
## END STANDARD BUILD SCRIPT INCLUDE
. "$KEYMAN_ROOT/resources/build/zip.inc.sh"
# This script runs from its parent's folder
cd "$(dirname "$THIS_SCRIPT")/.."
@ -68,16 +70,17 @@ FRAMEWORK="KeymanEngine.xcframework"
KEYMAN_SAMPLES="samples"
echo "engine dest: $KMEI_DST"
ZIP_FLAGS=("-q" "-r" "-X") # quiet, recursive, no-extra
echo "Zipping ${FRAMEWORK} => ${UPLOAD_DIR}/${KMEI_DST}..."
cd "${KMEI_FRAMEWORK_BASE}"
zip -qrX "${KMEI_DST}" ${FRAMEWORK}
add_zip_files "${KMEI_DST}" "${ZIP_FLAGS[@]}" "${FRAMEWORK}"
cd "$WORK_DIR"
echo "Copying Keyman Engine samples into ${UPLOAD_DIR}/${KMEI_DST_NAME}..."
cp -rf "${KEYMAN_SAMPLES}" "${UPLOAD_DIR}/samples"
cd "${UPLOAD_DIR}"
zip -qr "${KMEI_DST_NAME}" "samples"
add_zip_files "${KMEI_DST_NAME}" "-x@../../zip-excludes" "${ZIP_FLAGS[@]}" "samples"
rm -rf "samples"
cd "$WORK_DIR"
@ -97,7 +100,7 @@ KEYMANAPP_SIM_APP_DST="keyman-ios-simulator-${BUILD_NUMBER}.app.zip"
echo "Zipping Keyman simulator artifact ${KEYMANAPP_SIM_APP} => ${UPLOAD_DIR}/${KEYMANAPP_SIM_APP_DST}..."
cd "${KEYMANAPP_SIM_FOLDER}"
zip -qrX "${WORK_DIR}/${UPLOAD_DIR}/${KEYMANAPP_SIM_APP_DST}" "Keyman.app"
add_zip_files "${WORK_DIR}/${UPLOAD_DIR}/${KEYMANAPP_SIM_APP_DST}" "${ZIP_FLAGS[@]}" "Keyman.app"
echo "${WORK_DIR}/${UPLOAD_DIR}/${KEYMANAPP_SIM_APP_DST}"
cd "$WORK_DIR"
@ -118,7 +121,7 @@ if [ "${RELEASE_OEM_FIRSTVOICES}" = true ]; then
echo "Zipping FirstVoices simulator artifact ${FIRSTVOICESAPP_SIM_APP} => ${UPLOAD_DIR}/${KEYMANAPP_SIM_APP_DST}..."
cd "${FIRSTVOICESAPP_SIM_FOLDER}"
zip -qrX "${WORK_DIR}/${UPLOAD_DIR}/${FIRSTVOICESAPP_SIM_APP_DST}" "FirstVoices.app"
add_zip_files "${WORK_DIR}/${UPLOAD_DIR}/${FIRSTVOICESAPP_SIM_APP_DST}" "${ZIP_FLAGS[@]}" "FirstVoices.app"
cd "$WORK_DIR"
fi

2
ios/zip-excludes Normal file
View file

@ -0,0 +1,2 @@
samples/*/build.sh

View file

@ -15,12 +15,14 @@ sudo apt-get upgrade
sudo apt-get install keyman onboard-keyman
```
An older version of Keyman for Linux packages is also available in any current Debian at:
A possibly older version of Keyman for Linux packages is also available in
any current Debian at:
[https://packages.debian.org/keyman](https://packages.debian.org/keyman)
**Note** in order to fully work you'll need a patched version of ibus
which is available in the above launchpad repo. Patched versions of ibus
will have the string `sil` in the package version number.
**Note** Versions before Keyman 18 required a patched version of ibus in order
to fully work. Patched versions of ibus were available in the above mentioned
launchpad repository and had the string `sil` in the package version number.
Keyman 18+ no longer requires a patched version of ibus.
## Q. How do I install a Keyman keyboard?

View file

@ -62,14 +62,14 @@ build_action() {
builder_echo "Create version.py"
pushd keyman_config
sed \
-e "s/_KEYMAN_VERSION_/${KEYMAN_VERSION}/g" \
-e "s/_KEYMAN_VERSION_WITH_TAG_/${KEYMAN_VERSION_WITH_TAG}/g" \
-e "s/_KEYMAN_VERSION_GIT_TAG_/${KEYMAN_VERSION_GIT_TAG}/g" \
-e "s/_KEYMAN_VERSION_MAJOR_/${KEYMAN_VERSION_MAJOR}/g" \
-e "s/_KEYMAN_VERSION_RELEASE_/${KEYMAN_VERSION_RELEASE}/g" \
-e "s/_KEYMAN_TIER_/${KEYMAN_TIER}/g" \
-e "s/_KEYMAN_VERSION_ENVIRONMENT_/${KEYMAN_VERSION_ENVIRONMENT}/g" \
-e "s/_UPLOAD_SENTRY_/${UPLOAD_SENTRY}/g" \
-e "s/__KEYMAN_VERSION__/${KEYMAN_VERSION}/g" \
-e "s/__KEYMAN_VERSION_WITH_TAG__/${KEYMAN_VERSION_WITH_TAG}/g" \
-e "s/__KEYMAN_VERSION_GIT_TAG__/${KEYMAN_VERSION_GIT_TAG}/g" \
-e "s/__KEYMAN_VERSION_MAJOR__/${KEYMAN_VERSION_MAJOR}/g" \
-e "s/__KEYMAN_VERSION_RELEASE__/${KEYMAN_VERSION_RELEASE}/g" \
-e "s/__KEYMAN_TIER__/${KEYMAN_TIER}/g" \
-e "s/__KEYMAN_VERSION_ENVIRONMENT__/${KEYMAN_VERSION_ENVIRONMENT}/g" \
-e "s/__UPLOAD_SENTRY__/${UPLOAD_SENTRY}/g" \
version.py.in > version.py
popd
pushd buildtools

View file

@ -4,12 +4,12 @@
# 1) we don't load dependencies by storing it in __init__.py
# 2) we can import it in setup.py for the same reason
# 3) we can import it into your module module
__version__ = "_KEYMAN_VERSION_"
__versionwithtag__ = "_KEYMAN_VERSION_WITH_TAG_"
__majorversion__ = "_KEYMAN_VERSION_MAJOR_"
__releaseversion__ = "_KEYMAN_VERSION_RELEASE_"
__tier__ = "_KEYMAN_TIER_"
__version__ = "__KEYMAN_VERSION__"
__versionwithtag__ = "__KEYMAN_VERSION_WITH_TAG__"
__majorversion__ = "__KEYMAN_VERSION_MAJOR__"
__releaseversion__ = "__KEYMAN_VERSION_RELEASE__"
__tier__ = "__KEYMAN_TIER__"
__pkgversion__ = "(local)"
__environment__ = "_KEYMAN_VERSION_ENVIRONMENT_"
__uploadsentry__ = "_UPLOAD_SENTRY_"
__versiongittag__ = "_KEYMAN_VERSION_GIT_TAG_"
__environment__ = "__KEYMAN_VERSION_ENVIRONMENT__"
__uploadsentry__ = "__UPLOAD_SENTRY__"
__versiongittag__ = "__KEYMAN_VERSION_GIT_TAG__"

View file

@ -1,5 +1,5 @@
#!/bin/bash
PYTHONPATH=.:${PYTHONPATH}
export PYTHONPATH=.:${PYTHONPATH}
XDG_CONFIG_HOME=$(mktemp --directory)
export XDG_CONFIG_HOME
@ -13,6 +13,8 @@ if [[ "$1" == "--coverage" ]]; then
fi
if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
PYTHONPATH=$(dirname "$0")/../tools:${PYTHONPATH}
if ! pip3 list --format=columns | grep -q teamcity-messages; then
if [[ -n "${TEAMCITY_PLATFORM}" ]] || [[ -n "${DOCKER_RUNNING}" ]]; then
# Ubuntu 24.04+ prevents mixing pip and system packages and wants us
@ -29,7 +31,9 @@ if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
# shellcheck disable=SC2086
pip3 install --user ${PIP_ARGS:-} teamcity-messages
fi
test_module=teamcity.unittestpy
test_module=teamcity_pytestrunner.unittestpy
echo "##teamcity[testStarted name='|[keyman-config|] Running unit tests']"
echo "##teamcity[flowStarted flowId='unit_tests']"
else
test_module=unittest
extra_opts=-v
@ -38,4 +42,8 @@ fi
# shellcheck disable=SC2086
python3 ${coverage:-} -m "${test_module:-}" discover ${extra_opts:-} -s tests/ -p "*_tests.py"
if [[ -n "${TEAMCITY_GIT_PATH}" ]]; then
echo "##teamcity[flowFinished flowId='unit_tests']"
echo "##teamcity[testFinished name='|[keyman-config|] Finished running unit tests']"
fi
rm -rf "${XDG_CONFIG_HOME}"

View file

@ -78,5 +78,5 @@ function checkAndInstallRequirements()
sudo mk-build-deps debian/control
wait_for_apt_deb && sudo DEBIAN_FRONTEND="noninteractive" \
apt-get -qy --allow-downgrades install ./keyman-build-deps_*.deb
sudo rm -f keyman-build-deps_*
rm -f keyman-build-deps_*
}

View file

@ -32,14 +32,14 @@ quilt new version_py.diff
quilt add "version.py"
sed \
-e "s/_KEYMAN_VERSION_/${KEYMAN_VERSION}/g" \
-e "s/_KEYMAN_VERSION_WITH_TAG_/${KEYMAN_VERSION_WITH_TAG}/g" \
-e "s/_KEYMAN_VERSION_GIT_TAG_/${KEYMAN_VERSION_GIT_TAG}/g" \
-e "s/_KEYMAN_VERSION_MAJOR_/${KEYMAN_VERSION_MAJOR}/g" \
-e "s/_KEYMAN_VERSION_RELEASE_/${KEYMAN_VERSION_RELEASE}/g" \
-e "s/_KEYMAN_TIER_/${KEYMAN_TIER}/g" \
-e "s/_KEYMAN_VERSION_ENVIRONMENT_/${KEYMAN_VERSION_ENVIRONMENT}/g" \
-e "s/_UPLOAD_SENTRY_/${UPLOAD_SENTRY}/g" \
-e "s/__KEYMAN_VERSION__/${KEYMAN_VERSION}/g" \
-e "s/__KEYMAN_VERSION_WITH_TAG__/${KEYMAN_VERSION_WITH_TAG}/g" \
-e "s/__KEYMAN_VERSION_GIT_TAG__/${KEYMAN_VERSION_GIT_TAG}/g" \
-e "s/__KEYMAN_VERSION_MAJOR__/${KEYMAN_VERSION_MAJOR}/g" \
-e "s/__KEYMAN_VERSION_RELEASE__/${KEYMAN_VERSION_RELEASE}/g" \
-e "s/__KEYMAN_TIER__/${KEYMAN_TIER}/g" \
-e "s/__KEYMAN_VERSION_ENVIRONMENT__/${KEYMAN_VERSION_ENVIRONMENT}/g" \
-e "s/__UPLOAD_SENTRY__/${UPLOAD_SENTRY}/g" \
version.py.in > version.py
quilt refresh
quilt pop -a

View file

@ -0,0 +1,12 @@
# Teamcity Python Test Runner
This implements an improved version of JetBrains' python unittest runner for
Teamcity. It makes the output of the tests indented and collapsible under
the test block by specifying a parent flow.
Requires the `teamcity-messages` package.
## References
- <https://www.jetbrains.com/help/teamcity/service-messages.html#Message+FlowId>
- <https://github.com/JetBrains/teamcity-messages>

View file

@ -0,0 +1,61 @@
import sys
from teamcity.messages import TeamcityServiceMessages
from teamcity.unittestpy import TeamcityTestResult, TeamcityTestRunner
from unittest import main
class KeymanTeamcityServiceMessages(TeamcityServiceMessages):
def __init__(self, parent_flow='unit_tests'):
self.parent_flow = parent_flow
super().__init__()
def testStarted(self, testName, captureStandardOutput=None, flowId=None, metainfo=None):
self.message('testStarted', name=testName, metainfo=metainfo)
self.message('flowStarted', flowId=flowId, parent=self.parent_flow)
def testFinished(self, testName, testDuration=None, flowId=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
if testDuration is not None:
duration_ms = testDuration.days * 86400000 + \
testDuration.seconds * 1000 + \
int(testDuration.microseconds / 1000)
self.message('testFinished', name=testName, duration=str(duration_ms))
else:
self.message('testFinished', name=testName)
def testIgnored(self, testName, message='', flowId=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
self.message('testIgnored', name=testName, message=message)
def testFailed(self, testName, message='', details='', flowId=None, comparison_failure=None):
self.message('flowFinished', flowId=flowId, parent=self.parent_flow)
if not comparison_failure:
self.message('testFailed', name=testName, message=message, details=details)
else:
diff_message = u"\n{0} != {1}\n".format(comparison_failure.actual, comparison_failure.expected)
self.message(
'testFailed',
name=testName,
message=str(message) + diff_message,
details=details,
type="comparisonFailure",
actual=comparison_failure.actual,
expected=comparison_failure.expected,
)
class KeymanTestResult(TeamcityTestResult):
def __init__(self, stream=sys.stdout, descriptions=None, verbosity=None):
super().__init__(stream, descriptions, verbosity)
self.messages = KeymanTeamcityServiceMessages()
class KeymanTestRunner(TeamcityTestRunner):
resultclass = KeymanTestResult
def run(self, test):
return super().run(test)
if __name__ == '__main__':
main(module=None, testRunner=KeymanTestRunner())

View file

@ -69,16 +69,12 @@ PREPRELEASE=false
UPDATE_VERSION_IN_PLIST=true
DO_CODESIGN=true
CODESIGNING_SUPPRESSION="CODE_SIGN_IDENTITY=\"\" CODE_SIGNING_REQUIRED=NO"
QUIET=true
QUIET=false
if builder_verbose; then
BUILD_OPTIONS=""
QUIET_FLAG=
QUIET=false
else
BUILD_OPTIONS="-quiet"
QUIET_FLAG="-quiet"
fi
BUILD_ACTIONS="build"
@ -290,7 +286,7 @@ do_publish() {
builder_heading "Preparing files for release deployment..."
./setup/build.sh
"$KM4MIM_BASE_PATH/make-km-dmg.sh" $QUIET_FLAG
"$KM4MIM_BASE_PATH/make-km-dmg.sh"
# Create download info
"$KM4MIM_BASE_PATH/write-download_info.sh"

6
package-lock.json generated
View file

@ -12738,9 +12738,9 @@
}
},
"node_modules/tar-fs": {
"version": "3.0.8",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.8.tgz",
"integrity": "sha512-ZoROL70jptorGAlgAYiLoBLItEKw/fUxg9BSYK/dF/GAGYFJOJJJMvjPAKDJraCXFwadD456FCuvLWgfhMsPwg==",
"version": "3.0.9",
"resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-3.0.9.tgz",
"integrity": "sha512-XF4w9Xp+ZQgifKakjZYmFdkLoSWd34VGKcsTCwlNWM7QG3ZbaxnTsaBwnjFZqHRf/rROxaR8rXnbtwdvaDI+lA==",
"dev": true,
"license": "MIT",
"dependencies": {

View file

@ -41,8 +41,10 @@ type -t nvm >/dev/null || {
}
}
nvm use "$REQUIRED_NODE_VERSION" || \
(nvm install "$REQUIRED_NODE_VERSION" && nvm use "$REQUIRED_NODE_VERSION")
if ! nvm use "${REQUIRED_NODE_VERSION}"; then
nvm install "${REQUIRED_NODE_VERSION}"
nvm use "${REQUIRED_NODE_VERSION}"
fi
# Beware the hardcoded path below -- it should already be in the system PATH

View file

@ -166,8 +166,9 @@ while IFS= read -r line; do
# Which platform are we watching?
eval watch='$'watch_$platform
# Add common patterns to the watch list
watch="^($platform|(oem/[^/]+/$platform)|resources|$watch)"
if [[ "$line" =~ $watch ]]; then
watch="^(${platform}|(oem/[^/]+/${platform})|resources/((?!teamcity)|teamcity/(${platform}|includes))|$watch)"
# Since bash doesn't support negative look-aheads we use grep to test
if echo "${line}" | grep --quiet --perl-regexp "${watch}"; then
build_platforms+=($platform)
fi
fi

View file

@ -62,7 +62,7 @@ bc_test_common_linux=(Keyman_Test_Common_Linux)
vcs_test=HttpsGithubComKeymanappKeymanPRs
# Master (Alpha) Build Configurations; these diverge when we need new build configurations for new versions
# Master (Alpha) Build Configurations; these may diverge when we need new build configurations for new versions
bc_master_android=(KeymanAndroid_Build)
bc_master_ios=(Keyman_iOS_Master)
@ -86,57 +86,7 @@ bc_beta_developer=(Keyman_Developer_Release)
vcs_beta=HttpsGithubComKeymanappKeyman
# Stable 14.0 Build Configurations
bc_stable_14_0_android=(KeymanAndroid_Build)
bc_stable_14_0_ios=(Keyman_iOS_Master)
bc_stable_14_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub)
bc_stable_14_0_mac=(KeymanMac_Master)
bc_stable_14_0_windows=(Keyman_Build)
bc_stable_14_0_web=(Keymanweb_Build)
# 16.0+ Keyman Developer is split from Windows, but TC configuration will not fail
# on older versions, just no-op
bc_stable_14_0_developer=(Keyman_Developer_Release)
vcs_stable_14_0=HttpsGithubComKeymanappKeyman
# Stable 15.0 Build Configurations
bc_stable_15_0_android=(KeymanAndroid_Build)
bc_stable_15_0_ios=(Keyman_iOS_Master)
bc_stable_15_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub)
bc_stable_15_0_mac=(KeymanMac_Master)
bc_stable_15_0_windows=(Keyman_Build)
bc_stable_15_0_web=(Keymanweb_Build)
vcs_stable_15_0=HttpsGithubComKeymanappKeyman
# Stable 16.0 Build Configurations
bc_stable_16_0_android=(KeymanAndroid_Build)
bc_stable_16_0_ios=(Keyman_iOS_Master)
bc_stable_16_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub)
bc_stable_16_0_mac=(KeymanMac_Master)
bc_stable_16_0_windows=(Keyman_Build)
bc_stable_16_0_web=(Keymanweb_Build)
bc_stable_16_0_developer=(Keyman_Developer_Release)
vcs_stable_16_0=HttpsGithubComKeymanappKeyman
# Stable 17.0 Build Configurations
bc_stable_17_0_android=(KeymanAndroid_Build)
bc_stable_17_0_ios=(Keyman_iOS_Master)
bc_stable_17_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub)
bc_stable_17_0_mac=(KeymanMac_Master)
bc_stable_17_0_windows=(Keyman_Build)
bc_stable_17_0_web=(Keymanweb_Build)
bc_stable_17_0_developer=(Keyman_Developer_Release)
vcs_stable_17_0=HttpsGithubComKeymanappKeyman
# Stable 17.0 Build Configurations
# Stable 18.0 Build Configurations
bc_stable_18_0_android=(KeymanAndroid_Build)
bc_stable_18_0_ios=(Keyman_iOS_Master)
@ -147,3 +97,15 @@ bc_stable_18_0_web=(Keymanweb_Build)
bc_stable_18_0_developer=(Keyman_Developer_Release)
vcs_stable_18_0=HttpsGithubComKeymanappKeyman
# Stable 19.0 Build Configurations
bc_stable_19_0_android=(KeymanAndroid_Build)
bc_stable_19_0_ios=(Keyman_iOS_Master)
bc_stable_19_0_linux=(KeymanLinux_Master deb-release-packaging_GitHub)
bc_stable_19_0_mac=(KeymanMac_Master)
bc_stable_19_0_windows=(Keyman_Build)
bc_stable_19_0_web=(Keymanweb_Build)
bc_stable_19_0_developer=(Keyman_Developer_Release)
vcs_stable_19_0=HttpsGithubComKeymanappKeyman

View file

@ -8,13 +8,14 @@
#
# TODO: refactor with /resources/build/win/zip.inc.sh
# zip/7z to create an archive with the following parameters (in order)
# Add files to create a zip/7z archive with the following parameters (in order)
# [zip filename]
# [list of flags to pass to zip command. Flags start with a single-dash
# [list of flags to pass to zip command] Flags start with a single-dash
# -x@filename for a file containing list of files to exclude from the archive
# -* all other flags]
# -* all other flags
# Flags passed in are treated as zip parameters, and internally converterted to 7z flags as applicable
# [list of files to include in zip]
function zip_files() {
function add_zip_files() {
# Parse parameters
@ -23,15 +24,50 @@ function zip_files() {
shift
# Parse rest of parameters
FLAGS=()
INCLUDE=()
local ZIP_FLAGS=()
local SEVENZ_FLAGS=('a') # 7z requires a command
local INCLUDE=()
while [[ $# -gt 0 ]] ; do
case "$1" in
-*)
# zip/7z flags
FLAGS+=($1)
-r)
# recursive paths - Identical flag to zip and 7z
ZIP_FLAGS+=($1)
SEVENZ_FLAGS+=($1)
shift
;;
-x@*)
# Filename for a file containing list of files to exclude from the archive - Identical flag to zip and 7z
ZIP_FLAGS+=($1)
SEVENZ_FLAGS+=($1)
shift
;;
# Zip flags that have a corresponding 7z flag
-q)
# quiet mode -> disable progress indicator, set output log level 0
ZIP_FLAGS+=($1)
SEVENZ_FLAGS+=("-bd")
SEVENZ_FLAGS+=("-bb0")
shift
;;
-[0123456789])
# Compression level where
# -0 indicates no compression
# -1 indicates low compression (fastest)
# -9 indicates ultra compression (slowest)
ZIP_FLAGS+=($1)
if [[ $1 =~ -([0-9]) ]]; then
SEVENZ_FLAGS+=("-mx${BASH_REMATCH[1]}")
fi
shift;
;;
-*)
# Remaining zip flags that don't apply to 7z
ZIP_FLAGS+=($1)
shift
;;
*)
# files to include in the archive
INCLUDE+=($1)
@ -40,8 +76,7 @@ function zip_files() {
esac
done
COMPRESS_CMD=zip
SEVENZ_CMD=
local COMPRESS_CMD=zip
if ! command -v zip 2>&1 > /dev/null; then
# Fallback to 7z
if [[ -z "${SEVENZ+x}" ]]; then
@ -53,15 +88,18 @@ function zip_files() {
SEVENZ=7z
;;
esac
# 7z command to add files
COMPRESS_CMD="${SEVENZ}"
SEVENZ_CMD="a"
fi
# 7z command to add files so clear zip flags
COMPRESS_CMD="${SEVENZ}"
ZIP_FLAGS=()
else
# Using zip so clear 7z flags
SEVENZ_FLAGS=()
fi
# Create archive
builder_echo_debug "${COMPRESS_CMD} ${SEVENZ_CMD} ${FLAGS[@]} ${ZIP_FILE} ${INCLUDE[@]}"
"${COMPRESS_CMD}" ${SEVENZ_CMD} ${FLAGS[@]} ${ZIP_FILE} ${INCLUDE[@]}
# builder_echo_debug "${COMPRESS_CMD} ${SEVENZ_FLAGS[@]} ${ZIP_FLAGS[@]} ${ZIP_FILE} ${INCLUDE[@]}"
"${COMPRESS_CMD}" ${SEVENZ_FLAGS[@]} ${ZIP_FLAGS[@]} ${ZIP_FILE} ${INCLUDE[@]}
}

View file

@ -181,28 +181,30 @@ function builder_heading() {
builder_echo() {
local color=white message= mark= block= action= do_output=true
local color=white message= mark= block= action= do_output=true test=
local echo_target=echo
if [[ $# -gt 1 ]]; then
if [[ $1 =~ ^(white|grey|green|success|blue|heading|yellow|warning|red|error|purple|brightwhite|teal|debug|setmark)$ ]]; then
color="$1"
shift
elif [[ $1 == "start" ]]; then
elif [[ $1 == "start" ]] || [[ $1 == "startTest" ]]; then
# builder_echo start block message
action="$1"
test="$1"
block="$2"
shift 2
action="start"
color="heading"
if ! builder_is_running_on_teamcity && builder_is_child_build; then
do_output=${_builder_debug_internal:-false}
fi
elif [[ $1 == "end" ]]; then
elif [[ $1 == "end" ]] || [[ $1 == "endTest" ]]; then
# builder_echo end block status message
action="$1"
test="$1"
block="$2"
color="$3"
shift 3
action="end"
if [[ "${color}" != "error" ]] && ! builder_is_running_on_teamcity && builder_is_child_build; then
do_output=${_builder_debug_internal:-false}
fi
@ -211,7 +213,11 @@ builder_echo() {
message="$*"
if [[ "${action}" == "start" ]] && builder_is_running_on_teamcity; then
$echo_target -e "##teamcity[blockOpened name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
if [[ "${test}" == "startTest" ]]; then
$echo_target -e "##teamcity[testSuiteStarted name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
else
$echo_target -e "##teamcity[blockOpened name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
fi
fi
if ${do_output}; then
@ -241,7 +247,11 @@ builder_echo() {
fi
if [[ "${action}" == "end" ]] && builder_is_running_on_teamcity; then
$echo_target -e "##teamcity[blockClosed name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
if [[ "${test}" == "endTest" ]]; then
$echo_target -e "##teamcity[testSuiteFinished name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
else
$echo_target -e "##teamcity[blockClosed name='|[${THIS_SCRIPT_IDENTIFIER}|] ${block}']"
fi
fi
}

View file

@ -8,7 +8,7 @@
"homepage": "https://github.com/keymanapp/keyman",
"license": "MIT",
"bin": {
"gosh": "./gosh.js"
"gosh": "gosh.js"
},
"private": true,
"files": [
@ -18,6 +18,7 @@
"type": "git",
"url": "git+https://github.com/keymanapp/keyman.git"
},
"scripts": {},
"bugs": {
"url": "https://github.com/keymanapp/keyman/issues"
}

View file

@ -30,7 +30,7 @@ and then run the build, e.g.
```bash
cd /Develop
resources/teamcity/keyman-linux-test.sh configure,build,test
resources/teamcity/linux/keyman-linux-test.sh configure,build,test
```
NOTE: by default this will run the build as `root` in the container,

View file

@ -9,9 +9,10 @@ linux_clean_action() {
# Install required dependencies for building Keyman on Linux.
linux_install_dependencies_action() {
builder_heading "Installing dependencies"
builder_echo start "install dependencies" "Installing dependencies"
. "${KEYMAN_ROOT}/linux/scripts/package-build.inc.sh"
checkAndInstallRequirements
builder_echo end "install dependencies" success "Finished installing dependencies"
}
# Install additional dependencies required for determining test coverage.
@ -46,7 +47,8 @@ linux_build_action() {
# Run unit tests for Keyman for Linux.
linux_unit_tests_action() {
builder_echo start unit_tests "Running unit tests"
builder_echo startTest unit_tests "Running unit tests"
rm -f /tmp/ibus-engine-keyman.log
rm -f /tmp/ibus-daemon.log
# symlink might point to wrong location, so delete it - will be re-created during tests
@ -55,5 +57,5 @@ linux_unit_tests_action() {
export NO_AT_BRIDGE=1
# shellcheck disable=SC2068
"${KEYMAN_ROOT}/linux/build.sh" test $@
builder_echo end unit_tests success "Finished running unit tests"
builder_echo endTest unit_tests success "Finished running unit tests"
}

View file

@ -5,7 +5,7 @@
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/builder.inc.sh"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# shellcheck disable=SC2154
@ -22,7 +22,7 @@ builder_describe \
"configure install dependencies" \
"build make a release build" \
"test run unit tests" \
"publish make a source tarball" \
"publish make a source tarball and publish to downloads and launchpad" \
"--gpgkey=GPGKEYGRIP GPG key for signing" \
"--gpgpw=GPGKEYPW GPG key passphrase" \
"--rsync-path=RSYNC_PATH rsync path on remote server" \

View file

@ -6,7 +6,7 @@
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/builder.inc.sh"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# shellcheck disable=SC2154

View file

@ -6,7 +6,7 @@
## START STANDARD BUILD SCRIPT INCLUDE
# adjust relative paths as necessary
THIS_SCRIPT="$(readlink -f "${BASH_SOURCE[0]}")"
. "${THIS_SCRIPT%/*}/../../resources/build/builder.inc.sh"
. "${THIS_SCRIPT%/*}/../../../resources/build/builder.inc.sh"
## END STANDARD BUILD SCRIPT INCLUDE
# shellcheck disable=SC2154

View file

@ -1,7 +1,5 @@
#!/usr/bin/env bash
# Copyright (C) 2025 SIL International. All rights reserved.
# Distributed under the MIT License. See LICENSE.md file in the project
# root for full license information.
# Keyman is copyright (C) SIL Global. MIT License.
#
# TC build script to build release of KeymanWeb.
@ -28,8 +26,8 @@ builder_describe \
"configure install dependencies" \
"build build Web + embedded" \
"publish publish release" \
"--s.keyman.com=S_KEYMAN_COM_PATH path to s.keyman.com repository" \
"--help.keyman.com=HELP_KEYMAN_COM_PATH path to help.keyman.com repository"
"--s.keyman.com=S_KEYMAN_COM path to s.keyman.com repository" \
"--help.keyman.com=HELP_KEYMAN_COM path to help.keyman.com repository"
builder_parse "$@"
@ -40,10 +38,10 @@ function _push_release_to_skeymancom() {
# downloads.keyman.com so we can ensure files are available)
builder_echo start publish "Publishing release to s.keyman.com"
cd "${S_KEYMAN_COM_PATH:=${KEYMAN_ROOT}/../s.keyman.com}"
cd "${S_KEYMAN_COM:=${KEYMAN_ROOT}/../s.keyman.com}"
git pull https://github.com/keymanapp/s.keyman.com.git master
cd "${KEYMAN_ROOT}/web"
"${KEYMAN_ROOT}/web/ci.sh" prepare:s.keyman.com --s.keyman.com "${S_KEYMAN_COM_PATH}"
"${KEYMAN_ROOT}/web/ci.sh" prepare:s.keyman.com --s.keyman.com "${S_KEYMAN_COM}"
builder_echo end publish success "Finished publishing release to s.keyman.com"
}
@ -66,7 +64,7 @@ function _zip_and_upload_artifacts() {
function _upload_help() {
builder_echo start "upload help" "Uploading new Keyman for Web help to help.keyman.com"
export HELP_KEYMAN_COM="${HELP_KEYMAN_COM_PATH:-${KEYMAN_ROOT}/../help.keyman.com}"
export HELP_KEYMAN_COM="${HELP_KEYMAN_COM:-${KEYMAN_ROOT}/../help.keyman.com}"
cd "${KEYMAN_ROOT}/resources/build"
"${KEYMAN_ROOT}/resources/build/help-keyman-com.sh" web
cd "${KEYMAN_ROOT}/web"

View file

@ -1,7 +1,5 @@
#!/usr/bin/env bash
# Copyright (C) 2025 SIL International
# Distributed under the MIT License. See LICENSE.md file in the project
# root for full license information.
# Keyman is copyright (C) SIL Global. MIT License.
#
# TC build script for Keyman Web/Test

View file

@ -4,6 +4,7 @@
"type": "module",
"main": "build/index.js",
"license": "MIT",
"scripts": {},
"devDependencies": {
"marked": "^14.1.2",
"chalk": "^2.4.2"

View file

@ -28,7 +28,7 @@ if(!keymanweb) {
/**
* Do not enclose in an anonymous function, as the compiler may create
* global scope variables to replace true, false, null, whcih can then collide
* global scope variables to replace true, false, null, which can then collide
* with other variables.
* Instead, use the --output-wrapper command during optimization, which will
* add the anonymous function to enclose all code, including those optimized
@ -106,7 +106,7 @@ if(!keymanweb) {
* @param {Event} _id keyboard selection event
* @return {boolean}
*/
readonly _SelectKeyboard = (_id: Event) => {
private readonly _SelectKeyboard = async (_id: Event): Promise<boolean> => {
let id: string = '';
if(typeof(_id) == 'object') {
let t: HTMLElement = null;
@ -135,7 +135,7 @@ if(!keymanweb) {
_k.className='selected';
}
this._KMWSel = _k;
keymanweb.setActiveKeyboard(_name,_lgc);
await keymanweb.setActiveKeyboard(_name,_lgc);
} else {
_name=null;
}
@ -252,7 +252,7 @@ if(!keymanweb) {
*
* @param {Event} e event
*/
readonly _SelectorMouseOut = (e: MouseEvent) => {
private readonly _SelectorMouseOut = (e: MouseEvent) => {
if(keymanweb.activatingUI) {
keymanweb.activatingUI(0);
}

View file

@ -450,15 +450,15 @@ if(!keymanweb) {
* @param {Object} e event
* Description Change active keyboard in response to user selection event
*/
readonly SelectKeyboardChange = (e: Event) => {
private readonly SelectKeyboardChange = async (e: Event) => {
keymanweb.activatingUI(true);
if(this.KeyboardSelector.value != '-') {
const i=this.KeyboardSelector.selectedIndex;
const t=this.KeyboardSelector.options[i].value.split(':');
keymanweb.setActiveKeyboard(t[0],t[1]);
await keymanweb.setActiveKeyboard(t[0],t[1]);
} else {
keymanweb.setActiveKeyboard('');
await keymanweb.setActiveKeyboard('');
}
//if(osk['show']) osk['show'](osk['isEnabled']()); handled by keyboard change event???

View file

@ -187,7 +187,7 @@ if(!keymanweb) {
/**
* Toggle a single keyboard on or off - KMW button control event
**/
readonly switchSingleKbd = () => {
readonly switchSingleKbd = async () => {
const _v = keymanweb.getActiveKeyboard() == '';
let nLastKbd=0, kbdName='', lgCode='';
@ -202,10 +202,10 @@ if(!keymanweb) {
kbdName = this.keyboards[nLastKbd]._InternalName;
lgCode = this.keyboards[nLastKbd]._LanguageCode;
keymanweb.setActiveKeyboard(kbdName,lgCode);
await keymanweb.setActiveKeyboard(kbdName,lgCode);
this.lastActiveKeyboard = nLastKbd;
} else {
keymanweb.setActiveKeyboard('');
await keymanweb.setActiveKeyboard('');
}
if(this.kbdButton) {
@ -216,7 +216,7 @@ if(!keymanweb) {
/**
* Switch to the next keyboard in the list - KMW button control event
**/
readonly switchNextKbd = () => {
readonly switchNextKbd = async () => {
let _v = (keymanweb.getActiveKeyboard() == '');
let kbdName='', lgCode='';
@ -227,16 +227,16 @@ if(!keymanweb) {
kbdName = this.keyboards[0]._InternalName;
lgCode = this.keyboards[0]._LanguageCode;
keymanweb.setActiveKeyboard(kbdName,lgCode);
await keymanweb.setActiveKeyboard(kbdName,lgCode);
this.lastActiveKeyboard = 0;
} else {
if(this.lastActiveKeyboard == this.keyboards.length-1) {
keymanweb.setActiveKeyboard('');
await keymanweb.setActiveKeyboard('');
_v = false;
} else {
kbdName = this.keyboards[++this.lastActiveKeyboard]._InternalName;
lgCode = this.keyboards[this.lastActiveKeyboard]._LanguageCode;
keymanweb.setActiveKeyboard(kbdName,lgCode);
await keymanweb.setActiveKeyboard(kbdName,lgCode);
_v = true;
}
}
@ -558,7 +558,7 @@ if(!keymanweb) {
* @param {number} _kbd
* Description Select a keyboard from the drop down menu
**/
selectKbd(_kbd: number) {
private async selectKbd(_kbd: number): Promise<boolean> {
let _name,_lgCode;
if(_kbd < 0) {
_name = '';
@ -568,7 +568,7 @@ if(!keymanweb) {
_lgCode = this.keyboards[_kbd]._LanguageCode;
}
keymanweb.setActiveKeyboard(_name,_lgCode);
await keymanweb.setActiveKeyboard(_name,_lgCode);
keymanweb.focusLastActiveElement();
this.kbdButton._setSelected(_name != '');
if(_kbd >= 0) {

View file

@ -820,7 +820,7 @@ if(!keymanweb) {
* @param {boolean} updateKeyman
* @return {boolean}
**/
selectKeyboard(event: Event, lang: LanguageEntry, kbd: KeyboardDetail, updateKeyman: boolean) {
private async selectKeyboard(event: Event, lang: LanguageEntry, kbd: KeyboardDetail, updateKeyman: boolean) {
keymanweb.activatingUI(true);
if(this.selectedLanguage) {
@ -841,7 +841,7 @@ if(!keymanweb) {
// Return focus to input area and activate the selected keyboard
this.addKeyboardToList(lang, kbd);
if(updateKeyman) {
keymanweb.setActiveKeyboard(kbd.InternalName, kbd.LanguageCode).then(() => {
await keymanweb.setActiveKeyboard(kbd.InternalName, kbd.LanguageCode).then(() => {
// Restore focus _after_ the keyboard finishes loading.
this.setLastFocus();
});
@ -943,7 +943,7 @@ if(!keymanweb) {
* @return {boolean}
* Description Update the UI when all keyboards disabled by user
**/
readonly offButtonClickEvent = (event: Event) => {
private readonly offButtonClickEvent = async (event: Event) => {
if(this.toolbarNode.className != 'kmw_controls_disabled') {
this.hideKeyboardsForLanguage(null);
if(this.selectedLanguage) {
@ -959,7 +959,7 @@ if(!keymanweb) {
// Return the focus to the input area and set the active keyboard to nothing
this.setLastFocus();
keymanweb.setActiveKeyboard('','');
await keymanweb.setActiveKeyboard('','');
//Save current state when deselecting a keyboard (may not be needed)
this.saveCookie();

View file

@ -12,8 +12,8 @@ type Options = {
};
export class KeymanSentryManager {
keymanPlatform: string;
_enabled: boolean = true;
private keymanPlatform: string;
private _enabled: boolean = true;
static STANDARD_ALIASABLE_FILES = {
'keymanweb.js': 'keymanweb.js',
@ -34,7 +34,7 @@ export class KeymanSentryManager {
}
// If we've recognized one of our source files,
aliasFilename(filename: string): string|null {
private aliasFilename(filename: string): string|null {
if(!this.mayAlias(filename)) {
return null;
}
@ -49,14 +49,14 @@ export class KeymanSentryManager {
}
}
mayAlias(filename: string): boolean {
private mayAlias(filename: string): boolean {
return !!KeymanSentryManager.STANDARD_ALIASABLE_FILES[filename];
}
// Filters all expected but unnecessary path prefixes, affixes, and suffixes reported by Sentry from our products.
// This allows us to mask all different sorts of installations with a single uploaded path.
// Modifies original object.
pathFilter(event: any) {
private pathFilter(event: any) {
// Get the underlying JS error.
let exception = event.exception;
if(!exception) {
@ -90,7 +90,7 @@ export class KeymanSentryManager {
}
// Attaches some useful debugging information to the specified object, pass-by-reference style.
attachEventMetadata(event: any) {
private attachEventMetadata(event: any) {
// Ensure that the 'extra' object exists. (May not exist for synthetic/custom Errors.)
event.extra = event.extra || {};
//@ts-ignore
@ -100,7 +100,7 @@ export class KeymanSentryManager {
// Sanitizes the event object (in-place) to remove sensitive information
// from the breadcrumbs and url (for embedded KeymanWeb)
sanitizeEvent(event: any) {
private sanitizeEvent(event: any) {
if (event && event.breadcrumbs) {
event.breadcrumbs.forEach((b: any) => {
if (b.category == 'navigation') {
@ -123,7 +123,7 @@ export class KeymanSentryManager {
* Also will sanitize the Sentry event.
* @param event A Sentry-generated event
*/
prepareEvent(event: any): boolean {
private prepareEvent(event: any): boolean {
this.pathFilter(event);
this.attachEventMetadata(event);
this.sanitizeEvent(event);
@ -148,7 +148,7 @@ export class KeymanSentryManager {
* if we want to prevent the event from being sent to the server.
* @param event
*/
prepareEventDebugWrapper(event: any) {
public prepareEventDebugWrapper(event: any) {
if(DEBUG) {
try {
if(this.prepareEvent(event)) {
@ -178,7 +178,7 @@ export class KeymanSentryManager {
*
* https://stackoverflow.com/a/53214615/1836776
*/
initConsole() {
private initConsole() {
// creating function declarations for better stacktraces (otherwise they'd be anonymous function expressions)
let oldConsoleError = console.error;
let _this = this;
@ -223,7 +223,7 @@ export class KeymanSentryManager {
}
}
init() {
public init() {
// Do the actual Sentry initialization.
//@ts-ignore
Sentry.init({
@ -238,11 +238,11 @@ export class KeymanSentryManager {
this.initConsole();
}
get enabled(): boolean {
public get enabled(): boolean {
return this._enabled;
}
set enabled(value: boolean) {
public set enabled(value: boolean) {
this._enabled = value;
}
}

View file

@ -987,10 +987,11 @@ begin
executeResult := WaitForElevatedConfiguration(0, '-ikp');
if (executeResult <> 0) then
begin
TKeymanSentryClient.Client.MessageEvent
(Sentry.Client.SENTRY_LEVEL_ERROR,
'Executing kmshell process to install keyboard packages failed:"' +
IntToStr(Ord(executeResult)) + '"');
TKeymanSentryClient.Breadcrumb('error',
'Executing kmshell process to install keyboard packages failed"' +
IntToStr(Ord(executeResult)) + '"', 'update');
KL.Log('InstallingState.LaunchInstallPackageProcess failed executing kmshell ' +
'process to install keyboard packages: "' + IntToStr(Ord(executeResult)) + '"');
ChangeState(IdleState);
end;
end
@ -1042,9 +1043,10 @@ begin
if not FResult then
begin
bucStateContext.RemoveCachedFiles;
TKeymanSentryClient.Client.MessageEvent(Sentry.Client.SENTRY_LEVEL_ERROR,
'Executing kmshell process to install failed:"' +
IntToStr(Ord(FResult)) + '"');
TKeymanSentryClient.Breadcrumb('error',
'InstallingState.DoInstallKeyman: failed executing kmshell or file not found', 'update');
KL.Log('InstallingState.DoInstallKeyman failed executing kmshell ' +
'or the update was not found in the cache');
ChangeState(IdleState);
end;