Merge branch 'master' into feat/core/9467-devolve-regex-to-js2-epic-ldml

This commit is contained in:
Steven R. Loomis 2024-06-13 14:29:01 -05:00 • committed by GitHub
commit ff3c9736f4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
159 changed files with 8945 additions and 2257 deletions

View file

@ -37,7 +37,6 @@ jobs:
id: environment_step
run: |
cat artifacts/env >> $GITHUB_OUTPUT
cat artifacts/env
- name: Set pending status on PR builds
id: set_status
@ -68,22 +67,14 @@ jobs:
- name: "Verify API for libkeymancore*.so (${{ steps.environment_step.outputs.GIT_BRANCH }}, branch ${{ steps.environment_step.outputs.GIT_BASE_BRANCH }}, by ${{ steps.environment_step.outputs.GIT_USER }})"
run: |
echo "VERSION=${{ steps.environment_step.outputs.VERSION }}"
echo "PRERELEASE_TAG=${{ steps.environment_step.outputs.PRERELEASE_TAG }}"
echo "GIT_SHA=${{ steps.environment_step.outputs.GIT_SHA }}"
echo "GIT_BASE=${{ steps.environment_step.outputs.GIT_BASE }}"
ls -al ${GITHUB_WORKSPACE}
ls -al ${GITHUB_WORKSPACE}/artifacts
BIN_PACKAGE=$(ls "${GITHUB_WORKSPACE}/artifacts/" | grep "${PKG_NAME}[0-9]*_${{ steps.environment_step.outputs.VERSION }}-1${{ steps.environment_step.outputs.PRERELEASE_TAG }}+$(lsb_release -c -s)1_amd64.deb")
echo "BIN_PACKAGE=${BIN_PACKAGE}"
ls -al ${BIN_PACKAGE}
cd ${{ github.workspace }}/keyman/linux
./scripts/deb-packaging.sh \
--gha \
--bin-pkg "${BIN_PACKAGE}" \
--bin-pkg "${GITHUB_WORKSPACE}/artifacts/${BIN_PACKAGE}" \
--git-sha "${{ steps.environment_step.outputs.GIT_SHA }}" \
--git-base "${{ steps.environment_step.outputs.GIT_BASE }}" \
verify # 2>> $GITHUB_STEP_SUMMARY
verify 2>> $GITHUB_STEP_SUMMARY
- name: Archive .symbols file
uses: actions/upload-artifact@26f96dfa697d77e81fd5907df203aa23a56210a8 # v4.3.0
@ -99,23 +90,22 @@ jobs:
if: ${{ always() && needs.api_verification.outputs.IS_TEST_BUILD == 'true' }}
steps:
- name: Set success
# TEMP commented until things are working!
# if: needs.api_verification.result == 'success'
if: needs.api_verification.result == 'success'
run: |
echo "RESULT=success" >> $GITHUB_ENV
echo "MSG=Package build succeeded" >> $GITHUB_ENV
# - name: Set cancelled
# if: needs.api_verification.result == 'cancelled'
# run: |
# echo "RESULT=error" >> $GITHUB_ENV
# echo "MSG=Package build cancelled" >> $GITHUB_ENV
- name: Set cancelled
if: needs.api_verification.result == 'cancelled'
run: |
echo "RESULT=error" >> $GITHUB_ENV
echo "MSG=Package build cancelled" >> $GITHUB_ENV
# - name: Set failure
# if: needs.api_verification.result == 'failure'
# run: |
# echo "RESULT=failure" >> $GITHUB_ENV
# echo "MSG=Package build failed" >> $GITHUB_ENV
- name: Set failure
if: needs.api_verification.result == 'failure'
run: |
echo "RESULT=failure" >> $GITHUB_ENV
echo "MSG=Package build failed" >> $GITHUB_ENV
- name: Set final status
run: |

View file

@ -1,5 +1,66 @@
# Keyman Version History
## 18.0.55 alpha 2024-06-13
* fix(developer): handle missing OSK when importing a Windows keyboard into a touch-only project (#11720)
* fix(developer): verify email addresses in .kps and .keyboard_info (#11735)
* change(web): prep for better asynchronous prediction handling (#10343)
## 18.0.54 alpha 2024-06-12
* fix(common): remove subpackage entries for older TS version (#11745)
* chore(common): end use of ts-node (#11746)
* feat(web): add bulk_render variant that loads and renders keyboards from local KMP (#10432)
## 18.0.53 alpha 2024-06-10
* fix(android): check current orientation when redisplaying system keyboard (#11604)
* fix(android): fix keyboard size after rotation and restore via onSizeChanged, after layout (#11722)
* fix(developer): fix kmcmplib unit-test include paths (#11749)
## 18.0.52 alpha 2024-06-08
* fix(developer): prevent two touch layout editors opening for the same file (#11717)
* chore(common): cleanup meson deprecations and warnings (#11523)
* feat(developer): support language reference in context help (#11737)
* test(developer): kmcmplib compiler unit tests (#11378)
## 18.0.51 alpha 2024-06-07
* fix(web): fix osk touch-focus tracking (#11705)
* fix(web): defer keyboard activation requests made during engine initialization (#11713)
* chore(developer): add context/character-map (#11656)
* chore(developer): add context/wordlist-editor (#11658)
* chore(developer): add context/new-model-project-parameters (#11677)
* fix(common): remove allowJs from web's tsconfig.base.json (#11718)
* change(web): precompile all TS-based tests (#11723)
* chore(developer): add extra logging for assertion failure when pressing backspace in debugger (#11707)
* chore: add cherry-pick information in commit messages (#11708)
* fix(developer): handle encoding errors when loading wordlists (#11711)
* chore(ios): remove dead Swift-side keyboard gesture code (#11672)
* fix(mac): change build configuration to prevent cycle error in Xcode 15 (#11730)
* refactor(web): Replace deprecated substr with substring (#11637)
## 18.0.50 alpha 2024-06-06
* chore(common): adds retry mechanism for build script npm ci calls (#11451)
* fix(web): get row-height for flick constraints after performing layout (#11691)
* chore(ios): enable webview debugging (#11229)
* fix(android): handle `IllegalArgumentException` when initializing `CloudDownloadMgr`, add logging to check for unhandled side-effects (#11626)
* chore: replace git dep on restructure with 3.0.1 in npm (#11657)
* chore: move xml2js into the repo to eliminate npm git dependency (#11660)
* chore(common): move CLDR import copy into build step for common/web/types (#11690)
* fix(developer): handle editor initializing after debugger when setting execution point (#11587)
* fix(developer): treat js files with unrecognized encodings as non-keyboard files (#11698)
* fix(developer): disable example edit controls if no examples in Package Editor (#11701)
## 18.0.49 alpha 2024-06-05
* fix(web): revert #11598 to eliminate use of `finalInput` which caused crash after moving caret (#11685)
* fix(common): correctly display result of multiple option parameters in builder (#11679)
* fix(linux): specify path with package name (#11694)
* fix(linux): remove debug output and re-enable failures (#11695)
## 18.0.48 alpha 2024-06-04
* fix(linux): add debug output (#11668)

View file

@ -1 +1 @@
18.0.49
18.0.56

View file

@ -147,6 +147,16 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
KMManager.onStartInput(attribute, restarting);
KMManager.resetContext(KeyboardType.KEYBOARD_TYPE_SYSTEM);
// This method (likely) includes the IME equivalent to `onResume` for `Activity`-based classes,
// making it an important time to detect orientation changes.
Context appContext = getApplicationContext();
int newOrientation = KMManager.getOrientation(appContext);
if(newOrientation != lastOrientation) {
lastOrientation = newOrientation;
Configuration newConfig = this.getResources().getConfiguration();
KMManager.onConfigurationChanged(newConfig);
}
// Temporarily disable predictions on certain fields (e.g. hidden password field or numeric)
int inputType = attribute.inputType;
KMManager.setMayPredictOverride(inputType);
@ -154,7 +164,6 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
KMManager.setBannerOptions(false);
} else if (KMManager.isKeyboardLoaded(KeyboardType.KEYBOARD_TYPE_SYSTEM)){
// Check if predictions needs to be re-enabled per Settings preference
Context appContext = getApplicationContext();
Keyboard kbInfo = KMManager.getCurrentKeyboardInfo(appContext);
if (kbInfo != null) {
String langId = kbInfo.getLanguageID();
@ -171,7 +180,7 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
ExtractedText icText = ic.getExtractedText(new ExtractedTextRequest(), 0);
/*
We do sometimes receive null `icText.text`, even though
getExtractedText() docs does not list this as a possible
getExtractedText() docs does not list this as a possible
return value, so we test for that as well (#11479)
*/
if (icText != null && icText.text != null) {
@ -199,15 +208,6 @@ public class SystemKeyboard extends InputMethodService implements OnKeyboardEven
super.onUpdateExtractingVisibility(ei);
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
if (newConfig.orientation != lastOrientation) {
lastOrientation = newConfig.orientation;
KMManager.onConfigurationChanged(newConfig);
}
}
@Override
public void onConfigureWindow(Window win, boolean isFullscreen, boolean isCandidatesOnly) {
super.onConfigureWindow(win, isFullscreen, isCandidatesOnly);

View file

@ -145,7 +145,7 @@ function setOskHeight(h) {
function setOskWidth(w) {
if(w > 0) {
oskWidth = w;
oskWidth = w / window.devicePixelRatio;
}
}

View file

@ -195,13 +195,13 @@ final class KMKeyboard extends WebView {
int selMin = icText.selectionStart, selMax = icText.selectionEnd;
int textLength = rawText.length();
if (selMin < 0 || selMax < 0) {
// There is no selection or cursor
// Reference https://developer.android.com/reference/android/text/Selection#getSelectionEnd(java.lang.CharSequence)
return false;
} else if (selMin > textLength || selMax > textLength) {
// Selection is past end of existing text -- should not be possible but we
// Selection is past end of existing text -- should not be possible but we
// are seeing it happen; #11506
return false;
}
@ -231,7 +231,7 @@ final class KMKeyboard extends WebView {
selMin -= pairsAtStart;
selMax -= (pairsAtStart + pairsSelected);
this.loadJavascript(KMString.format("updateKMSelectionRange(%d,%d)", selMin, selMax));
return true;
}
@ -262,7 +262,7 @@ final class KMKeyboard extends WebView {
// When `.isTestMode() == true`, the setWebContentsDebuggingEnabled method is not available
// and thus will trigger unit-test failures.
if (!KMManager.isTestMode() && (
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 ||
(context.getApplicationInfo().flags & ApplicationInfo.FLAG_DEBUGGABLE) != 0 ||
KMManager.getTier(null) != KMManager.Tier.STABLE
)) {
// Enable debugging of WebView via adb. Not used during unit tests
@ -443,20 +443,15 @@ final class KMKeyboard extends WebView {
dismissHelpBubble();
}
@Override
public void onConfigurationChanged(Configuration newConfig) {
super.onConfigurationChanged(newConfig);
RelativeLayout.LayoutParams params = KMManager.getKeyboardLayoutParams();
// I suspect this is the part we should actually be calling directly...
this.setLayoutParams(params);
int bannerHeight = KMManager.getBannerHeight(context);
int oskHeight = KMManager.getKeyboardHeight(context);
if (this.htmlBannerString != null && !this.htmlBannerString.isEmpty()) {
setHTMLBanner(this.htmlBannerString);
}
loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight));
loadJavascript(KMString.format("setOskWidth(%d)", newConfig.screenWidthDp));
loadJavascript(KMString.format("setOskHeight(%d)", oskHeight));
this.invalidate();
this.requestLayout();
this.dismissHelpBubble();
@ -465,6 +460,27 @@ final class KMKeyboard extends WebView {
}
}
@Override
public void onSizeChanged(int width, int height, int oldWidth, int oldHeight) {
super.onSizeChanged(width, height, oldWidth, oldHeight);
int bannerHeight = KMManager.getBannerHeight(context);
int oskHeight = KMManager.getKeyboardHeight(context);
if(bannerHeight + oskHeight != height) {
// We'll proceed, but cautiously and with logging.
KMLog.LogInfo(TAG, "Height mismatch: onSizeChanged = " + height + ", our version = " + (bannerHeight + oskHeight));
}
if (this.htmlBannerString != null && !this.htmlBannerString.isEmpty()) {
setHTMLBanner(this.htmlBannerString);
}
loadJavascript(KMString.format("setBannerHeight(%d)", bannerHeight));
loadJavascript(KMString.format("setOskWidth(%d)", width));
// Must be last - it's the one that triggers a Web-engine layout refresh.
loadJavascript(KMString.format("setOskHeight(%d)", oskHeight));
}
public void dismissSuggestionMenuWindow() {
try {
if (suggestionMenuWindow != null && suggestionMenuWindow.isShowing()) {

View file

@ -40,6 +40,7 @@ public class CloudDownloadMgr{
{
if(instance!=null)
return;
KMLog.LogBreadcrumb("CloudDownloadMgr", "CloudDownloadMgr.createInstance() - first call", true);
instance = new CloudDownloadMgr();
}
@ -60,6 +61,7 @@ public class CloudDownloadMgr{
if(isInitialized)
return;
try {
KMLog.LogBreadcrumb("CloudDownloadMgr", "attempting CloudDownloadMgr.initialize()", true);
// Runtime-registered boradcasts receivers must specify export behavior to indicate whether
// or not the receiver should be exported to all other apps on the device
// https://developer.android.com/about/versions/14/behavior-changes-14#runtime-receivers-exported
@ -69,11 +71,12 @@ public class CloudDownloadMgr{
} else {
aContext.registerReceiver(completeListener, new IntentFilter(DownloadManager.ACTION_DOWNLOAD_COMPLETE));
}
} catch (IllegalStateException e) {
} catch (IllegalArgumentException e) {
String message = "initialize error: ";
KMLog.LogException(TAG, message, e);
}
isInitialized = true;
KMLog.LogBreadcrumb("CloudDownloadMgr", ".initialize() call complete", false);
}
/**
@ -82,11 +85,15 @@ public class CloudDownloadMgr{
*/
public synchronized void shutdown(Context aContext)
{
if(!isInitialized)
if(!isInitialized) {
return;
}
KMLog.LogBreadcrumb("CloudDownloadMgr", "CloudDownloadMgr.shutdown()", true);
try {
aContext.unregisterReceiver(completeListener);
} catch (IllegalStateException e) {
} catch (IllegalArgumentException e) {
String message = "shutdown error: ";
KMLog.LogException(TAG, message, e);
}
@ -206,8 +213,10 @@ public class CloudDownloadMgr{
CloudApiTypes.CloudApiParam... params)
{
if(!isInitialized) {
Log.w(TAG, "Downloadmanager not initialized. Initializing CloudDownloadMgr.");
Log.w(TAG, "DownloadManager not initialized. Initializing CloudDownloadMgr.");
initialize(aContext);
} else {
KMLog.LogBreadcrumb("CloudDownloadMgr", "CloudDownloadMgr.executeAsDownload() called; already initialized", true);
}
synchronized (downloadSetByDownloadIdentifier) {
@ -218,7 +227,7 @@ public class CloudDownloadMgr{
DownloadManager downloadManager = (DownloadManager) aContext.getSystemService(Context.DOWNLOAD_SERVICE);
if(downloadManager==null)
throw new IllegalStateException("Downloadmanager is not available");
throw new IllegalStateException("DownloadManager is not available");
aCallback.initializeContext(aContext);

View file

@ -13,6 +13,7 @@ import com.keyman.engine.KMManager;
import com.keyman.engine.util.DependencyUtil;
import com.keyman.engine.util.DependencyUtil.LibraryType;
import io.sentry.Breadcrumb;
import io.sentry.Sentry;
import io.sentry.SentryLevel;
@ -34,6 +35,48 @@ public final class KMLog {
}
}
/**
* Utility to log info and add as a Sentry breadcrumb, rather than
* as an independent message
* @param tag String of the caller
* @param msg String of the info message
*/
public static void LogBreadcrumb(String tag, String msg, boolean addStackTrace) {
if (msg == null || msg.isEmpty()) {
return;
}
Log.i(tag, msg);
if (!DependencyUtil.libraryExists(LibraryType.SENTRY) || !Sentry.isEnabled()) {
return;
}
Breadcrumb crumb = new Breadcrumb();
crumb.setMessage(msg);
crumb.setLevel(SentryLevel.INFO);
if(addStackTrace) {
StackTraceElement[] rawTrace = Thread.currentThread().getStackTrace();
// The call that gets us the stack-trace above... shows up in the
// stack trace, so we'll skip the first few (redundant) entries.
int skipCount = 3;
// Sentry does limit the size of messages... so let's just
// keep 10 entries and call it a day.
int limit = Math.min(rawTrace.length, 10 + skipCount);
if(rawTrace.length > skipCount) {
String[] trace = new String[limit - skipCount];
for (int i = skipCount; i < limit; i++) {
trace[i-skipCount] = rawTrace[i].toString();
}
crumb.setData("stacktrace", trace);
}
}
Sentry.addBreadcrumb(crumb);
}
/**
* Utility to log error and send to Sentry
* @param tag String of the caller

View file

@ -57,7 +57,7 @@
"c8": "^7.12.0",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"dependencies": {
"@keymanapp/keyman-version": "*",

View file

@ -29,7 +29,7 @@
},
"homepage": "https://github.com/keymanapp/keyman/tree/master/common/models/types#readme",
"devDependencies": {
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"files": [
"README.md",

View file

@ -54,8 +54,7 @@
"c8": "^7.12.0",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"type": "module"
}

View file

@ -46,8 +46,7 @@
"@keymanapp/resources-gosh": "*",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"dependencies": {
"@keymanapp/models-templates": "*",

View file

@ -2,6 +2,9 @@ import unwrap from '../unwrap.js';
import { LMLayerWorkerCode, LMLayerWorkerSourcemapComment } from "@keymanapp/lm-worker/worker-main.wrapped.js";
export default class SourcemappedWorker {
// the only difference to DefaultWorker is that this class uses
// the unminified LM* blobs
static constructInstance(): Worker {
return new Worker(this.asBlobURI(LMLayerWorkerCode));
}

View file

@ -5,6 +5,6 @@
"license": "MIT",
"devDependencies": {
"@keymanapp/resources-gosh": "*",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
}
}

View file

@ -20,8 +20,7 @@
"@keymanapp/resources-gosh": "*",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"dependencies": {
"convert-source-map": "^2.0.0"

View file

@ -1,5 +0,0 @@
import { fileURLToPath } from "url";
import { dirname } from 'path';
// Tells ts-node where to find the tsconfig.json to be used for executing TS unit tests.
process.env.TS_NODE_PROJECT = `${dirname(fileURLToPath(import.meta.url))}/src/test/auto/tsconfig.json`;

View file

@ -1,10 +0,0 @@
{
"extensions": ["js", "ts"],
"spec" : [
"src/test/auto/headless/**/*.*"
],
"node-option": [
"loader=ts-node/esm"
],
"require": ".mocharc.env.js"
}

View file

@ -6,8 +6,7 @@
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"promise-status-async": "^1.2.10",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"imports": {
"#tools": "./build/tools/obj/index.js"

View file

@ -1,5 +1,5 @@
import { assert } from 'chai'
import sinon from 'sinon';
import sinon from 'sinon';
import path from 'path';
import url from 'url';
import fs from 'fs';
@ -8,9 +8,11 @@ import { GestureDebugPath } from '@keymanapp/gesture-recognizer';
import { timedPromise } from '@keymanapp/web-utils';
import { TouchpathTurtle } from '#tools';
// Ensures that the resources are resolved relative to this script, not to the cwd when the test
// runner was launched.
const scriptFolder = path.dirname(url.fileURLToPath(import.meta.url));
// Ensures that the resources are resolved relative to this script's source, not
// to the cwd when the test runner was launched or to its built version.
const scriptFolder = path.dirname(url.fileURLToPath(import.meta.url))
.replace("build/test/auto", "src/test/auto") // Mac/Linux
.replace("build\\test\\auto", "src\\test\\auto"); // Windows
const SEGMENT_TEST_JSON_FOLDER = path.resolve(`${scriptFolder}/../../resources/json/segmentation`);
describe("GesturePath", function() {

View file

@ -1,15 +0,0 @@
/*
* VS Code Intellisense needs this helper in order to properly use subpath imports in the
* test specs found under the `headless` subfolder. Otherwise, it'll report errors while
* editing - even if the tests themselves actually work.
*/
{
"extends": "../../../../tsconfig.kmw-main-base.json",
"compilerOptions": {
// Not needed when testing via Node, and when `true` it seems to desync preset breakpoints
// worse than when `false`.
"importHelpers": false,
// Makes the tests... somewhat tricky to handle properly.
"noImplicitAny": false
}
}

View file

@ -0,0 +1,47 @@
declare module 'promise-status-async' {
export type PromisePredicate = (p: Promise<any>) => Promise<boolean>
export enum PromiseStatuses {
PROMISE_PENDING = 'pending',
PROMISE_RESOLVED = 'resolved',
PROMISE_REJECTED = 'rejected'
}
export type PromisePendingState = {
status: PromiseStatuses.PROMISE_PENDING
}
export type PromiseResolvedState<T = any> = {
status: PromiseStatuses.PROMISE_RESOLVED,
value: T
}
export type PromiseRejectedState<T = any> = {
status: PromiseStatuses.PROMISE_REJECTED,
reason: T
}
export type PromiseState<T = any, R = any> = PromisePendingState | PromiseResolvedState<T> | PromiseRejectedState<R>
export const promiseState: <T = any, R = any>(promise: T | Promise<T>) => PromiseState<T, R>
export const promiseStatus: <T>(promise: T | Promise<T>) => PromiseStatuses
const PROMISE_PENDING: PromiseStatuses.PROMISE_PENDING
const PROMISE_RESOLVED: PromiseStatuses.PROMISE_RESOLVED
const PROMISE_REJECTED: PromiseStatuses.PROMISE_REJECTED
const PromisePredicates: {
isPromisePending: PromisePredicate,
isPromiseResolved: PromisePredicate,
isPromiseRejected: PromisePredicate,
isPromiseNotPending: PromisePredicate,
isPromiseNotResolved: PromisePredicate,
isPromiseNotRejected: PromisePredicate
}
const isPromisePending: PromisePredicate
const isPromiseResolved: PromisePredicate
const isPromiseRejected: PromisePredicate
const isPromiseNotPending: PromisePredicate
const isPromiseNotResolved: PromisePredicate
const isPromiseNotRejected: PromisePredicate
}

View file

@ -1,6 +1,5 @@
import { InputSample, buildGestureMatchInspector, GestureSource, gestures } from '@keymanapp/gesture-recognizer';
import { InputSample, buildGestureMatchInspector, GestureSource, GestureSourceSubview, gestures } from '@keymanapp/gesture-recognizer';
import { ManagedPromise, timedPromise } from '@keymanapp/web-utils';
import { GestureSourceSubview } from '../../../build/obj/headless/gestureSource.js';
type GestureMatcher<Type> = gestures.matchers.GestureMatcher<Type>;
type MatcherSelection<Type> = gestures.matchers.MatcherSelection<Type>;
@ -80,7 +79,7 @@ function prepareSourcesFromPriorMatcher<Type>(
): ReturnType<typeof prepareSimContact<Type>> {
const spec = contactSpec;
const existingSources = spec.matcher.sources.map((src) => {
return src instanceof GestureSourceSubview<Type> ? src.baseSource : src;
return src instanceof GestureSourceSubview ? src.baseSource : src;
});
const sequences = spec.continuation;
@ -330,7 +329,7 @@ export function simulateMultiSourceMatcherInput<Type>(
const config: SimulationConfig<GestureMatcher<Type>, Type> = {
construction: (source) => new gestures.matchers.GestureMatcher<Type>(modelSpec, source),
addSource: (obj, source) => {
if(source instanceof GestureSource<Type>) {
if(source instanceof GestureSource) {
obj.addContact(source)
} else {
throw new Error("Error in internal sim-engine configuration");

View file

@ -0,0 +1,30 @@
/*
* VS Code Intellisense needs this helper in order to properly use subpath imports in the
* test specs found under the `headless` subfolder. Otherwise, it'll report errors while
* editing - even if the tests themselves actually work.
*/
{
"extends": "../../tsconfig.json",
"compilerOptions": {
"outDir": "../../build/test/",
// Not needed when testing via Node, and when `true` it seems to desync preset breakpoints
// worse than when `false`.
"importHelpers": false,
"allowJs": true,
"baseUrl": "../../",
"tsBuildInfoFile": "../../build/test/tsconfig.tsbuildinfo",
"rootDir": "./",
"typeRoots": ["./resources/@types", "../../../../../node_modules/@types"]
},
"include": [
"./auto/headless/**/*.ts",
"./resources/**/*.ts"
],
// Undo the base config's exclude.
"exclude": ["../../../../../node_modules/promise-status-async/lib/index.d.ts"],
"references": [
{"path": "../../"}
]
}

View file

@ -45,7 +45,12 @@ test-headless ( ) {
MOCHA_FLAGS="$MOCHA_FLAGS --reporter mocha-teamcity-reporter"
fi
c8 mocha --recursive $MOCHA_FLAGS ./src/test/auto/headless/
# The currently-bundled declaration file for this package generates errors when compiling against it
# with current tsc versions.
rm -f "${KEYMAN_ROOT}/node_modules/promise-status-async/lib/index.d.ts"
tsc -b ./src/test/tsconfig.json
c8 mocha --recursive $MOCHA_FLAGS ./build/test/auto/headless/
}
test-browser ( ) {

View file

@ -21,8 +21,7 @@
"@keymanapp/resources-gosh": "*",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"scripts": {
"test": "gosh ./test.sh"

View file

@ -21,8 +21,7 @@
"c8": "^7.12.0",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"scripts": {
"build": "gosh build.sh",

View file

@ -13,6 +13,6 @@
"license": "MIT",
"type": "module",
"devDependencies": {
"typescript": "^4.9.5"
"typescript": "^5.4.5"
}
}

View file

@ -7,7 +7,7 @@
},
"license": "MIT",
"devDependencies": {
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"files": [
"message.d.ts"

View file

@ -34,8 +34,7 @@
"combine-source-map": "^0.8.0",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"ts-node": "^10.9.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"dependencies": {
"@keymanapp/keyman-version": "*",

View file

@ -328,12 +328,12 @@ export default class LMLayerWorker {
switch(payload.message) {
case 'predict':
var {transform, context} = payload;
var suggestions = compositor.predict(transform, context);
// Now that the suggestions are ready, send them out!
this.cast('suggestions', {
token: payload.token,
suggestions: suggestions
compositor.predict(transform, context).then((suggestions) => {
// Now that the suggestions are ready, send them out!
this.cast('suggestions', {
token: payload.token,
suggestions: suggestions
});
});
break;
case 'wordbreak':
@ -358,11 +358,12 @@ export default class LMLayerWorker {
break;
case 'revert':
var {reversion, context} = payload;
var suggestions: Suggestion[] = compositor.applyReversion(reversion, context);
this.cast('postrevert', {
token: payload.token,
suggestions: suggestions
compositor.applyReversion(reversion, context).then((suggestions) => {
this.cast('postrevert', {
token: payload.token,
suggestions: suggestions
});
});
break;
case 'reset-context':

View file

@ -65,7 +65,7 @@ export default class ModelCompositor {
return returnedPredictions;
}
predict(transformDistribution: Transform | Distribution<Transform>, context: Context): Suggestion[] {
async predict(transformDistribution: Transform | Distribution<Transform>, context: Context): Promise<Suggestion[]> {
let suggestionDistribution: Distribution<Suggestion> = [];
let lexicalModel = this.lexicalModel;
let punctuation = this.punctuation;
@ -249,21 +249,11 @@ export default class ModelCompositor {
// Worth considering: extend Traversal to allow direct prediction lookups?
// let traversal = match.finalTraversal;
// Find a proper Transform ID to map the correction to.
// Without it, we can't apply the suggestion.
let finalInput: Transform;
if(match.inputSequence.length > 0) {
// common case: from the same keystroke `inputTransform`, with matching `.id`.
finalInput = match.inputSequence[match.inputSequence.length - 1].sample;
} else {
finalInput = inputTransform; // A fallback measure. Greatly matters for empty contexts.
}
// Replace the existing context with the correction.
let correctionTransform: Transform = {
insert: correction, // insert correction string
deleteLeft: deleteLeft,
id: finalInput.id // The correction should always be based on the most recent external transform/transcription ID.
id: inputTransform.id // The correction should always be based on the most recent external transform/transcription ID.
}
let rootCost = match.totalCost;
@ -658,18 +648,19 @@ export default class ModelCompositor {
return reversion;
}
applyReversion(reversion: Reversion, context: Context): Suggestion[] {
async applyReversion(reversion: Reversion, context: Context): Promise<Suggestion[]> {
// If we are unable to track context (because the model does not support LexiconTraversal),
// we need a "fallback" strategy.
let compositor = this;
let fallbackSuggestions = function() {
let fallbackSuggestions = async function() {
let revertedContext = models.applyTransform(reversion.transform, context);
let suggestions = compositor.predict({insert: '', deleteLeft: 0}, revertedContext);
const suggestions = await compositor.predict({insert: '', deleteLeft: 0}, revertedContext);
suggestions.forEach(function(suggestion) {
// A reversion's transform ID is the additive inverse of its original suggestion;
// we revert to the state of said original suggestion.
suggestion.transformId = -reversion.transformId;
});
return suggestions;
}

View file

@ -8,7 +8,7 @@ import DummyModel from '#./models/dummy-model.js';
import ModelCompositor from '#./model-compositor.js';
describe('Custom Punctuation', function () {
it('appears in the keep suggestion', function () {
it('appears in the keep suggestion', async function () {
let dummySuggestions = [{
transform: {
insert: 'Hello',
@ -37,7 +37,7 @@ describe('Custom Punctuation', function () {
// The model compositor is responsible for adding this to the display as
// string.
var composite = new ModelCompositor(model, true);
var suggestions = composite.predict([{ sample: { insert: 'o', deleteLeft: 0 }, p: 1.00 }], {
var suggestions = await composite.predict([{ sample: { insert: 'o', deleteLeft: 0 }, p: 1.00 }], {
left: 'Hrll', startOfBuffer: false, endOfBuffer: true
});
assert.lengthOf(suggestions, 3);
@ -54,7 +54,7 @@ describe('Custom Punctuation', function () {
});
describe("insertAfterWord", function () {
it('appears after "word" suggestion', function () {
it('appears after "word" suggestion', async function () {
let dummySuggestions = [
{
transform: { insert: 'ᚈᚑᚋ', deleteLeft: 0, },
@ -82,7 +82,7 @@ describe('Custom Punctuation', function () {
// The model compositor is responsible for adding this to the display as
// string.
var composite = new ModelCompositor(model, true);
var suggestions = composite.predict([{ sample: { insert: 'ᚋ', deleteLeft: 0 }, p: 1.00 }], {
var suggestions = await composite.predict([{ sample: { insert: 'ᚋ', deleteLeft: 0 }, p: 1.00 }], {
left: '᚛ᚈᚑ', startOfBuffer: false, endOfBuffer: true
});
assert.lengthOf(suggestions, dummySuggestions.length);

View file

@ -19,7 +19,7 @@ describe('ModelCompositor', function() {
{wordBreaker: wordBreakers.default}
);
it('generates suggestions with expected properties', function() {
it('generates suggestions with expected properties', async function() {
let compositor = new ModelCompositor(plainModel, true);
let context = {
left: 'th', startOfBuffer: true, endOfBuffer: true,
@ -30,7 +30,7 @@ describe('ModelCompositor', function() {
deleteLeft: 0
};
let suggestions = compositor.predict(inputTransform, context);
let suggestions = await compositor.predict(inputTransform, context);
suggestions.forEach(function(suggestion) {
// Suggstions are built based on the context state BEFORE the triggering
// input, replacing the prediction's root with the complete word.
@ -55,7 +55,7 @@ describe('ModelCompositor', function() {
});
});
it('strongly avoids corrections for single-character roots', function() {
it('strongly avoids corrections for single-character roots', async function() {
let compositor = new ModelCompositor(plainModel, true);
let context = {
left: '', startOfBuffer: true, endOfBuffer: true,
@ -68,8 +68,8 @@ describe('ModelCompositor', function() {
{sample: {insert: 'a', deleteLeft: 0}, p: 0.4} // but at lower weight than 'and' (998).
];
compositor.predict({insert: '', deleteLeft: 0}, context); // Initialize context tracking first!
let suggestions = compositor.predict(inputDistribution, context);
await compositor.predict({insert: '', deleteLeft: 0}, context); // Initialize context tracking first!
let suggestions = await compositor.predict(inputDistribution, context);
// remove the keep suggestion; we're not testing that here.
suggestions = suggestions.filter((suggestion) => suggestion.tag != 'keep');
@ -94,7 +94,7 @@ describe('ModelCompositor', function() {
assert.isUndefined(aRange.find((suggestion) => suggestion.transform.insert.charAt(0) == 'q'));
});
it('properly handles suggestions after a backspace', function() {
it('properly handles suggestions after a backspace', async function() {
let compositor = new ModelCompositor(plainModel, true);
let context = {
left: 'the ', startOfBuffer: true, endOfBuffer: true,
@ -105,7 +105,7 @@ describe('ModelCompositor', function() {
deleteLeft: 1
};
let suggestions = compositor.predict(inputTransform, context);
let suggestions = await compositor.predict(inputTransform, context);
suggestions.forEach(function(suggestion) {
// Suggestions always delete the full root of the suggestion.
//
@ -117,7 +117,7 @@ describe('ModelCompositor', function() {
});
});
it('properly handles suggestions for the first letter after a ` `', function() {
it('properly handles suggestions for the first letter after a ` `', async function() {
let compositor = new ModelCompositor(plainModel, true);
let context = {
left: 'the', startOfBuffer: true, endOfBuffer: true,
@ -128,7 +128,7 @@ describe('ModelCompositor', function() {
deleteLeft: 0
};
let suggestions = compositor.predict(inputTransform, context);
let suggestions = await compositor.predict(inputTransform, context);
suggestions.forEach(function(suggestion) {
// After a space, predictions are based on a new, zero-length root.
// With nothing to replace, .deleteLeft should be zero.
@ -136,7 +136,7 @@ describe('ModelCompositor', function() {
});
});
it('properly handles suggestions for the first letter after a `\'`', function() {
it('properly handles suggestions for the first letter after a `\'`', async function() {
let compositor = new ModelCompositor(plainModel, true);
let context = {
left: "the '", startOfBuffer: true, endOfBuffer: true,
@ -149,7 +149,7 @@ describe('ModelCompositor', function() {
deleteLeft: 0
};
let suggestions = compositor.predict(inputTransform, context);
let suggestions = await compositor.predict(inputTransform, context);
suggestions.forEach(function(suggestion) {
// Suggestions always delete the full root of the suggestion.
// Which, here, didn't exist before the input. Nothing to
@ -320,20 +320,20 @@ describe('ModelCompositor', function() {
}
);
it('should produce suggestions from uncased input', function() {
it('should produce suggestions from uncased input', async function() {
let model = uncasedModel;
var composite = new ModelCompositor(model, true);
var compositor = new ModelCompositor(model, true);
// Initialize context
let context = {
left: 'th', startOfBuffer: false, endOfBuffer: true,
};
composite.predict({insert: '', deleteLeft: 0}, context);
await compositor.predict({insert: '', deleteLeft: 0}, context);
// Pretend to fat finger "the" as "thr"
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
var suggestions = composite.predict([thr, the], context);
var suggestions = await compositor.predict([thr, the], context);
// Get the top suggest for 'the' and 'thr*'.
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'the' || s.displayAs === '“the”'; })[0];
@ -348,20 +348,20 @@ describe('ModelCompositor', function() {
assert.isAbove(theSuggestion.p, thrSuggestion.p);
});
it('should not produce suggestions from cased input', function() {
it('should not produce suggestions from cased input', async function() {
let model = uncasedModel;
var composite = new ModelCompositor(model, true);
var compositor = new ModelCompositor(model, true);
// Initialize context
let context = {
left: 'TH', startOfBuffer: false, endOfBuffer: true,
};
composite.predict({insert: '', deleteLeft: 0}, context);
await compositor.predict({insert: '', deleteLeft: 0}, context);
// Pretend to fat finger "the" as "thr"
var the = { sample: { insert: 'R', deleteLeft: 0}, p: 0.45 };
var thr = { sample: { insert: 'E', deleteLeft: 0}, p: 0.55 };
var suggestions = composite.predict([thr, the], context);
var suggestions = await compositor.predict([thr, the], context);
// We should only receive a 'keep' suggestion.
assert.equal(suggestions.length, 1);
@ -395,20 +395,20 @@ describe('ModelCompositor', function() {
}
);
it('should produce suggestions from uncased input', function() {
it('should produce suggestions from uncased input', async function() {
let model = casedModel;
var composite = new ModelCompositor(model, true);
let compositor = new ModelCompositor(model, true);
// Initialize context
let context = {
left: 'th', startOfBuffer: false, endOfBuffer: true,
};
composite.predict({insert: '', deleteLeft: 0}, context);
await compositor.predict({insert: '', deleteLeft: 0}, context);
// Pretend to fat finger "the" as "thr"
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
var suggestions = composite.predict([thr, the], context);
var suggestions = await compositor.predict([thr, the], context);
// Get the top suggest for 'the' and 'thr*'.
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'the' || s.displayAs === '“the”'; })[0];
@ -423,20 +423,20 @@ describe('ModelCompositor', function() {
assert.isAbove(theSuggestion.p, thrSuggestion.p);
});
it('should produce capitalized suggestions from fully-uppercased input', function() {
it('should produce capitalized suggestions from fully-uppercased input', async function() {
let model = casedModel;
var composite = new ModelCompositor(model, true);
let compositor = new ModelCompositor(model, true);
// Initialize context
let context = {
left: 'TH', startOfBuffer: false, endOfBuffer: true,
};
composite.predict({insert: '', deleteLeft: 0}, context);
await compositor.predict({insert: '', deleteLeft: 0}, context);
// Pretend to fat finger "the" as "thr"
var the = { sample: { insert: 'R', deleteLeft: 0}, p: 0.45 };
var thr = { sample: { insert: 'E', deleteLeft: 0}, p: 0.55 };
var suggestions = composite.predict([thr, the], context);
var suggestions = await compositor.predict([thr, the], context);
// Get the top suggest for 'the' and 'thr*'.
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'THE' || s.displayAs === '“THE”'; })[0];
@ -451,20 +451,20 @@ describe('ModelCompositor', function() {
assert.isAbove(theSuggestion.p, thrSuggestion.p);
});
it('should produce "initial-case" suggestions from input with an initial capital', function() {
it('should produce "initial-case" suggestions from input with an initial capital', async function() {
let model = casedModel;
var composite = new ModelCompositor(model, true);
let compositor = new ModelCompositor(model, true);
// Initialize context
let context = {
left: 'Th', startOfBuffer: false, endOfBuffer: true,
};
composite.predict({insert: '', deleteLeft: 0}, context);
await compositor.predict({insert: '', deleteLeft: 0}, context);
// Pretend to fat finger "the" as "thr"
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
var suggestions = composite.predict([thr, the], context);
var suggestions = await compositor.predict([thr, the], context);
// Get the top suggest for 'the' and 'thr*'.
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'The' || s.displayAs === '“The”'; })[0];
@ -479,20 +479,20 @@ describe('ModelCompositor', function() {
assert.isAbove(theSuggestion.p, thrSuggestion.p);
});
it('also from input with partial capitalization when including an initial capital', function() {
it('also from input with partial capitalization when including an initial capital', async function() {
let model = casedModel;
var composite = new ModelCompositor(model, true);
let compositor = new ModelCompositor(model, true);
// Initialize context
let context = {
left: 'TH', startOfBuffer: false, endOfBuffer: true,
};
composite.predict({insert: '', deleteLeft: 0}, context);
await compositor.predict({insert: '', deleteLeft: 0}, context);
// Pretend to fat finger "the" as "thr"
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
var suggestions = composite.predict([thr, the], context);
var suggestions = await compositor.predict([thr, the], context);
// Get the top suggest for 'the' and 'thr*'.
var theSuggestion = suggestions.filter(function (s) { return s.displayAs.startsWith('The'); })[0];
@ -508,22 +508,22 @@ describe('ModelCompositor', function() {
});
describe('Prediction with legacy Models (12.0 / 13.0)', function() {
it('should compose suggestions from a fat-fingered keypress (no keying needed)', function () {
it('should compose suggestions from a fat-fingered keypress (no keying needed)', async function () {
var model = new TrieModel(
jsonFixture('models/tries/english-1000')
);
var composite = new ModelCompositor(model, true);
let compositor = new ModelCompositor(model, true);
// Initialize context
let context = {
left: 'th', startOfBuffer: false, endOfBuffer: true,
};
composite.predict({insert: '', deleteLeft: 0}, context);
await compositor.predict({insert: '', deleteLeft: 0}, context);
// Pretend to fat finger "the" as "thr"
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
var suggestions = composite.predict([thr, the], context);
var suggestions = await compositor.predict([thr, the], context);
// Get the top suggest for 'the' and 'thr*'.
var theSuggestion = suggestions.filter(function (s) { return s.displayAs === 'the' || s.displayAs === '“the”'; })[0];
@ -538,22 +538,22 @@ describe('ModelCompositor', function() {
assert.isAbove(theSuggestion.p, thrSuggestion.p);
});
it('should compose suggestions from a fat-fingered keypress (keying needed)', function () {
it('should compose suggestions from a fat-fingered keypress (keying needed)', async function () {
var model = new TrieModel(
jsonFixture('models/tries/english-1000')
);
var composite = new ModelCompositor(model, true);
let compositor = new ModelCompositor(model, true);
// Initialize context
let context = {
left: 'Th', startOfBuffer: false, endOfBuffer: true,
};
composite.predict({insert: '', deleteLeft: 0}, context);
await compositor.predict({insert: '', deleteLeft: 0}, context);
// Pretend to fat finger "the" as "thr"
var the = { sample: { insert: 'r', deleteLeft: 0}, p: 0.45 };
var thr = { sample: { insert: 'e', deleteLeft: 0}, p: 0.55 };
var suggestions = composite.predict([thr, the], context);
var suggestions = await compositor.predict([thr, the], context);
// Get the top suggest for 'the' and 'thr*'.
// As of 15.0+, because of #5429, the keep suggestion `"The"` will not be merged
@ -841,7 +841,7 @@ describe('ModelCompositor', function() {
// While this isn't a state the LMLayer should ever operate within, this provides
// a useful base state for developing further tests against the method.
it('model without traversals: returns appropriate suggestions upon reversion', function() {
it('model without traversals: returns appropriate suggestions upon reversion', async function() {
// This setup matches 'acceptSuggestion' the test case
// it('first word of context + postTransform provided, .deleteLeft > 0')
// seen earlier in the file.
@ -878,7 +878,7 @@ describe('ModelCompositor', function() {
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
assert.equal(appliedContext.left, "hello ");
let suggestions = compositor.applyReversion(reversion, appliedContext);
let suggestions = await compositor.applyReversion(reversion, appliedContext);
// As this test is a bit... 'hard-wired', we only get the 'keep' suggestion.
// It should still be accurate, though.
@ -891,7 +891,7 @@ describe('ModelCompositor', function() {
assert.deepEqual(suggestions[0].transform, expectedTransform);
});
it('model with traversals: returns appropriate suggestions upon reversion', function() {
it('model with traversals: returns appropriate suggestions upon reversion', async function() {
// This setup matches 'acceptSuggestion' the test case
// it('first word of context + postTransform provided, .deleteLeft > 0')
// seen earlier in the file.
@ -911,7 +911,7 @@ describe('ModelCompositor', function() {
let model = new models.TrieModel(jsonFixture('models/tries/english-1000'), {punctuation: englishPunctuation});
let compositor = new ModelCompositor(model, true);
let initialSuggestions = compositor.predict(postTransform, baseContext);
let initialSuggestions = await compositor.predict(postTransform, baseContext);
let keepSuggestion = initialSuggestions[0];
assert.equal(keepSuggestion.tag, 'keep'); // corresponds to `postTransform`, but the transform isn't equal.
@ -921,13 +921,13 @@ describe('ModelCompositor', function() {
assert.equal(reversion.id, -baseSuggestion.id);
let appliedContext = models.applyTransform(baseSuggestion.transform, baseContext);
let reversionSuggestions = compositor.applyReversion(reversion, appliedContext);
let reversionSuggestions = await compositor.applyReversion(reversion, appliedContext);
// The returned suggestion list should match the original suggestion list.
assert.deepEqual(reversionSuggestions, initialSuggestions);
});
it('model with traversals: properly tracks context state', function() {
it('model with traversals: properly tracks context state', async function() {
// Could be merged with the previous test case, but I think it's good to have the error
// sets flagged separately.
@ -946,7 +946,7 @@ describe('ModelCompositor', function() {
let model = new models.TrieModel(jsonFixture('models/tries/english-1000'), {punctuation: englishPunctuation});
let compositor = new ModelCompositor(model, true);
let initialSuggestions = compositor.predict(postTransform, baseContext);
let initialSuggestions = await compositor.predict(postTransform, baseContext);
const suggestionContextState = compositor.contextTracker.newest;
let keepSuggestion = initialSuggestions[0];

View file

@ -8,10 +8,11 @@ import LMLayerWorker from '#./index.js';
import { configWorker, createMessageEventWithData, emptyContext, iGotDistractedByHazel,
importScriptsWith, randomToken, zeroTransform } from '@keymanapp/common-test-resources/model-helpers.mjs';
import { timedPromise } from '@keymanapp/web-utils';
describe('LMLayerWorker', function () {
describe('#predict()', function () {
it('should send back suggestions', function () {
it('should send back suggestions', async function () {
var suggestion = {
transform: {
insert: 'I ',
@ -63,6 +64,10 @@ describe('LMLayerWorker', function () {
context: emptyContext()
}));
// predict() is async, so we need to relinquish control flow temporarily
// in order for a return message to become available.
await timedPromise(500);
// Retrieve the internal 'dummy' suggestions for comparison.
var hazel = iGotDistractedByHazel();

View file

@ -25,6 +25,6 @@
"@keymanapp/web-utils": "*"
},
"devDependencies": {
"typescript": "^4.9.5"
"typescript": "^5.4.5"
}
}

View file

@ -17,7 +17,7 @@
},
"homepage": "https://github.com/keymanapp/keyman#readme",
"devDependencies": {
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"dependencies": {
"@keymanapp/keyman-version": "*",

View file

@ -18,7 +18,7 @@
},
"dependencies": {
"tslib": "^2.5.2",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"devDependencies": {
"@keymanapp/resources-gosh": "*",

View file

@ -89,10 +89,14 @@ function copy_cldr_imports() {
function do_configure() {
compile_schemas
copy_cldr_imports
verify_npm_setup
}
function do_build() {
copy_cldr_imports
tsc --build
}
function do_test() {
eslint .
tsc --build test
@ -103,6 +107,6 @@ function do_test() {
builder_run_action clean rm -rf ./build/ ./tsconfig.tsbuildinfo
builder_run_action configure do_configure
builder_run_action build tsc --build
builder_run_action build do_build
builder_run_action test do_test
builder_run_action publish builder_publish_npm

View file

@ -31,9 +31,10 @@
"dependencies": {
"@keymanapp/ldml-keyboard-constants": "*",
"@keymanapp/keyman-version": "*",
"restructure": "git+https://github.com/keymanapp/dependency-restructure.git#7a188a1e26f8f36a175d95b67ffece8702363dfc",
"restructure": "3.0.1",
"semver": "^7.5.2",
"xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829"
"sax": ">=0.6.0",
"xmlbuilder": "~11.0.0"
},
"devDependencies": {
"@types/chai": "^4.1.7",
@ -50,8 +51,7 @@
"git-diff": "^2.0.6",
"hexy": "^0.3.4",
"mocha": "^8.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"mocha": {
"spec": "build/test/**/test-*.js",
@ -70,6 +70,7 @@
],
"exclude-after-remap": true,
"exclude": [
"src/deps/",
"src/kmx/kmx-plus-builder/",
"src/kmx/kmx-plus.ts",
"src/kmx/kmx-builder.ts",

View file

@ -0,0 +1,19 @@
Copyright 2010, 2011, 2012, 2013. All rights reserved.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to
deal in the Software without restriction, including without limitation the
rights to use, copy, modify, merge, publish, distribute, sublicense, and/or
sell copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS
IN THE SOFTWARE.

View file

@ -0,0 +1,507 @@
node-xml2js
===========
Ever had the urge to parse XML? And wanted to access the data in some sane,
easy way? Don't want to compile a C parser, for whatever reason? Then xml2js is
what you're looking for!
Description
===========
Simple XML to JavaScript object converter. It supports bi-directional conversion.
Uses [sax-js](https://github.com/isaacs/sax-js/) and
[xmlbuilder-js](https://github.com/oozcitak/xmlbuilder-js/).
Note: If you're looking for a full DOM parser, you probably want
[JSDom](https://github.com/tmpvar/jsdom).
Installation
============
Simplest way to install `xml2js` is to use [npm](http://npmjs.org), just `npm
install xml2js` which will download xml2js and all dependencies.
xml2js is also available via [Bower](http://bower.io/), just `bower install
xml2js` which will download xml2js and all dependencies.
Usage
=====
No extensive tutorials required because you are a smart developer! The task of
parsing XML should be an easy one, so let's make it so! Here's some examples.
Shoot-and-forget usage
----------------------
You want to parse XML as simple and easy as possible? It's dangerous to go
alone, take this:
```javascript
var parseString = require('xml2js').parseString;
var xml = "<root>Hello xml2js!</root>"
parseString(xml, function (err, result) {
console.dir(result);
});
```
Can't get easier than this, right? This works starting with `xml2js` 0.2.3.
With CoffeeScript it looks like this:
```coffeescript
{parseString} = require 'xml2js'
xml = "<root>Hello xml2js!</root>"
parseString xml, (err, result) ->
console.dir result
```
If you need some special options, fear not, `xml2js` supports a number of
options (see below), you can specify these as second argument:
```javascript
parseString(xml, {trim: true}, function (err, result) {
});
```
Simple as pie usage
-------------------
That's right, if you have been using xml-simple or a home-grown
wrapper, this was added in 0.1.11 just for you:
```javascript
var fs = require('fs'),
xml2js = require('xml2js');
var parser = new xml2js.Parser();
fs.readFile(__dirname + '/foo.xml', function(err, data) {
parser.parseString(data, function (err, result) {
console.dir(result);
console.log('Done');
});
});
```
Look ma, no event listeners!
You can also use `xml2js` from
[CoffeeScript](https://github.com/jashkenas/coffeescript), further reducing
the clutter:
```coffeescript
fs = require 'fs',
xml2js = require 'xml2js'
parser = new xml2js.Parser()
fs.readFile __dirname + '/foo.xml', (err, data) ->
parser.parseString data, (err, result) ->
console.dir result
console.log 'Done.'
```
But what happens if you forget the `new` keyword to create a new `Parser`? In
the middle of a nightly coding session, it might get lost, after all. Worry
not, we got you covered! Starting with 0.2.8 you can also leave it out, in
which case `xml2js` will helpfully add it for you, no bad surprises and
inexplicable bugs!
Promise usage
-------------
```javascript
var xml2js = require('xml2js');
var xml = '<foo></foo>';
// With parser
var parser = new xml2js.Parser(/* options */);
parser.parseStringPromise(xml).then(function (result) {
console.dir(result);
console.log('Done');
})
.catch(function (err) {
// Failed
});
// Without parser
xml2js.parseStringPromise(xml /*, options */).then(function (result) {
console.dir(result);
console.log('Done');
})
.catch(function (err) {
// Failed
});
```
Parsing multiple files
----------------------
If you want to parse multiple files, you have multiple possibilities:
* You can create one `xml2js.Parser` per file. That's the recommended one
and is promised to always *just work*.
* You can call `reset()` on your parser object.
* You can hope everything goes well anyway. This behaviour is not
guaranteed work always, if ever. Use option #1 if possible. Thanks!
So you wanna some JSON?
-----------------------
Just wrap the `result` object in a call to `JSON.stringify` like this
`JSON.stringify(result)`. You get a string containing the JSON representation
of the parsed object that you can feed to JSON-hungry consumers.
Displaying results
------------------
You might wonder why, using `console.dir` or `console.log` the output at some
level is only `[Object]`. Don't worry, this is not because `xml2js` got lazy.
That's because Node uses `util.inspect` to convert the object into strings and
that function stops after `depth=2` which is a bit low for most XML.
To display the whole deal, you can use `console.log(util.inspect(result, false,
null))`, which displays the whole result.
So much for that, but what if you use
[eyes](https://github.com/cloudhead/eyes.js) for nice colored output and it
truncates the output with `…`? Don't fear, there's also a solution for that,
you just need to increase the `maxLength` limit by creating a custom inspector
`var inspect = require('eyes').inspector({maxLength: false})` and then you can
easily `inspect(result)`.
XML builder usage
-----------------
Since 0.4.0, objects can be also be used to build XML:
```javascript
var xml2js = require('xml2js');
var obj = {name: "Super", Surname: "Man", age: 23};
var builder = new xml2js.Builder();
var xml = builder.buildObject(obj);
```
will result in:
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root>
<name>Super</name>
<Surname>Man</Surname>
<age>23</age>
</root>
```
At the moment, a one to one bi-directional conversion is guaranteed only for
default configuration, except for `attrkey`, `charkey` and `explicitArray` options
you can redefine to your taste. Writing CDATA is supported via setting the `cdata`
option to `true`.
To specify attributes:
```javascript
var xml2js = require('xml2js');
var obj = {root: {$: {id: "my id"}, _: "my inner text"}};
var builder = new xml2js.Builder();
var xml = builder.buildObject(obj);
```
will result in:
```xml
<?xml version="1.0" encoding="UTF-8" standalone="yes"?>
<root id="my id">my inner text</root>
```
### Adding xmlns attributes
You can generate XML that declares XML namespace prefix / URI pairs with xmlns attributes.
Example declaring a default namespace on the root element:
```javascript
let obj = {
Foo: {
$: {
"xmlns": "http://foo.com"
}
}
};
```
Result of `buildObject(obj)`:
```xml
<Foo xmlns="http://foo.com"/>
```
Example declaring non-default namespaces on non-root elements:
```javascript
let obj = {
'foo:Foo': {
$: {
'xmlns:foo': 'http://foo.com'
},
'bar:Bar': {
$: {
'xmlns:bar': 'http://bar.com'
}
}
}
}
```
Result of `buildObject(obj)`:
```xml
<foo:Foo xmlns:foo="http://foo.com">
<bar:Bar xmlns:bar="http://bar.com"/>
</foo:Foo>
```
Processing attribute, tag names and values
------------------------------------------
Since 0.4.1 you can optionally provide the parser with attribute name and tag name processors as well as element value processors (Since 0.4.14, you can also optionally provide the parser with attribute value processors):
```javascript
function nameToUpperCase(name){
return name.toUpperCase();
}
//transform all attribute and tag names and values to uppercase
parseString(xml, {
tagNameProcessors: [nameToUpperCase],
attrNameProcessors: [nameToUpperCase],
valueProcessors: [nameToUpperCase],
attrValueProcessors: [nameToUpperCase]},
function (err, result) {
// processed data
});
```
The `tagNameProcessors` and `attrNameProcessors` options
accept an `Array` of functions with the following signature:
```javascript
function (name){
//do something with `name`
return name
}
```
The `attrValueProcessors` and `valueProcessors` options
accept an `Array` of functions with the following signature:
```javascript
function (value, name) {
//`name` will be the node name or attribute name
//do something with `value`, (optionally) dependent on the node/attr name
return value
}
```
Some processors are provided out-of-the-box and can be found in `lib/processors.js`:
- `normalize`: transforms the name to lowercase.
(Automatically used when `options.normalize` is set to `true`)
- `firstCharLowerCase`: transforms the first character to lower case.
E.g. 'MyTagName' becomes 'myTagName'
- `stripPrefix`: strips the xml namespace prefix. E.g `<foo:Bar/>` will become 'Bar'.
(N.B.: the `xmlns` prefix is NOT stripped.)
- `parseNumbers`: parses integer-like strings as integers and float-like strings as floats
E.g. "0" becomes 0 and "15.56" becomes 15.56
- `parseBooleans`: parses boolean-like strings to booleans
E.g. "true" becomes true and "False" becomes false
Options
=======
Apart from the default settings, there are a number of options that can be
specified for the parser. Options are specified by ``new Parser({optionName:
value})``. Possible options are:
* `attrkey` (default: `$`): Prefix that is used to access the attributes.
Version 0.1 default was `@`.
* `charkey` (default: `_`): Prefix that is used to access the character
content. Version 0.1 default was `#`.
* `explicitCharkey` (default: `false`) Determines whether or not to use
a `charkey` prefix for elements with no attributes.
* `trim` (default: `false`): Trim the whitespace at the beginning and end of
text nodes.
* `normalizeTags` (default: `false`): Normalize all tag names to lowercase.
* `normalize` (default: `false`): Trim whitespaces inside text nodes.
* `explicitRoot` (default: `true`): Set this if you want to get the root
node in the resulting object.
* `emptyTag` (default: `''`): what will the value of empty nodes be. In case
you want to use an empty object as a default value, it is better to provide a factory
function `() => ({})` instead. Without this function a plain object would
become a shared reference across all occurrences with unwanted behavior.
* `explicitArray` (default: `true`): Always put child nodes in an array if
true; otherwise an array is created only if there is more than one.
* `ignoreAttrs` (default: `false`): Ignore all XML attributes and only create
text nodes.
* `mergeAttrs` (default: `false`): Merge attributes and child elements as
properties of the parent, instead of keying attributes off a child
attribute object. This option is ignored if `ignoreAttrs` is `true`.
* `validator` (default `null`): You can specify a callable that validates
the resulting structure somehow, however you want. See unit tests
for an example.
* `xmlns` (default `false`): Give each element a field usually called '$ns'
(the first character is the same as attrkey) that contains its local name
and namespace URI.
* `explicitChildren` (default `false`): Put child elements to separate
property. Doesn't work with `mergeAttrs = true`. If element has no children
then "children" won't be created. Added in 0.2.5.
* `childkey` (default `$$`): Prefix that is used to access child elements if
`explicitChildren` is set to `true`. Added in 0.2.5.
* `preserveChildrenOrder` (default `false`): Modifies the behavior of
`explicitChildren` so that the value of the "children" property becomes an
ordered array. When this is `true`, every node will also get a `#name` field
whose value will correspond to the XML nodeName, so that you may iterate
the "children" array and still be able to determine node names. The named
(and potentially unordered) properties are also retained in this
configuration at the same level as the ordered "children" array. Added in
0.4.9.
* `charsAsChildren` (default `false`): Determines whether chars should be
considered children if `explicitChildren` is on. Added in 0.2.5.
* `includeWhiteChars` (default `false`): Determines whether whitespace-only
text nodes should be included. Added in 0.4.17.
* `async` (default `false`): Should the callbacks be async? This *might* be
an incompatible change if your code depends on sync execution of callbacks.
Future versions of `xml2js` might change this default, so the recommendation
is to not depend on sync execution anyway. Added in 0.2.6.
* `strict` (default `true`): Set sax-js to strict or non-strict parsing mode.
Defaults to `true` which is *highly* recommended, since parsing HTML which
is not well-formed XML might yield just about anything. Added in 0.2.7.
* `attrNameProcessors` (default: `null`): Allows the addition of attribute
name processing functions. Accepts an `Array` of functions with following
signature:
```javascript
function (name){
//do something with `name`
return name
}
```
Added in 0.4.14
* `attrValueProcessors` (default: `null`): Allows the addition of attribute
value processing functions. Accepts an `Array` of functions with following
signature:
```javascript
function (value, name){
//do something with `name`
return name
}
```
Added in 0.4.1
* `tagNameProcessors` (default: `null`): Allows the addition of tag name
processing functions. Accepts an `Array` of functions with following
signature:
```javascript
function (name){
//do something with `name`
return name
}
```
Added in 0.4.1
* `valueProcessors` (default: `null`): Allows the addition of element value
processing functions. Accepts an `Array` of functions with following
signature:
```javascript
function (value, name){
//do something with `name`
return name
}
```
Added in 0.4.6
Options for the `Builder` class
-------------------------------
These options are specified by ``new Builder({optionName: value})``.
Possible options are:
* `attrkey` (default: `$`): Prefix that is used to access the attributes.
Version 0.1 default was `@`.
* `charkey` (default: `_`): Prefix that is used to access the character
content. Version 0.1 default was `#`.
* `rootName` (default `root` or the root key name): root element name to be used in case
`explicitRoot` is `false` or to override the root element name.
* `renderOpts` (default `{ 'pretty': true, 'indent': ' ', 'newline': '\n' }`):
Rendering options for xmlbuilder-js.
* pretty: prettify generated XML
* indent: whitespace for indentation (only when pretty)
* newline: newline char (only when pretty)
* `xmldec` (default `{ 'version': '1.0', 'encoding': 'UTF-8', 'standalone': true }`:
XML declaration attributes.
* `xmldec.version` A version number string, e.g. 1.0
* `xmldec.encoding` Encoding declaration, e.g. UTF-8
* `xmldec.standalone` standalone document declaration: true or false
* `doctype` (default `null`): optional DTD. Eg. `{'ext': 'hello.dtd'}`
* `headless` (default: `false`): omit the XML header. Added in 0.4.3.
* `allowSurrogateChars` (default: `false`): allows using characters from the Unicode
surrogate blocks.
* `cdata` (default: `false`): wrap text nodes in `<![CDATA[ ... ]]>` instead of
escaping when necessary. Does not add `<![CDATA[ ... ]]>` if it is not required.
Added in 0.4.5.
`renderOpts`, `xmldec`,`doctype` and `headless` pass through to
[xmlbuilder-js](https://github.com/oozcitak/xmlbuilder-js).
Updating to new version
=======================
Version 0.2 changed the default parsing settings, but version 0.1.14 introduced
the default settings for version 0.2, so these settings can be tried before the
migration.
```javascript
var xml2js = require('xml2js');
var parser = new xml2js.Parser(xml2js.defaults["0.2"]);
```
To get the 0.1 defaults in version 0.2 you can just use
`xml2js.defaults["0.1"]` in the same place. This provides you with enough time
to migrate to the saner way of parsing in `xml2js` 0.2. We try to make the
migration as simple and gentle as possible, but some breakage cannot be
avoided.
So, what exactly did change and why? In 0.2 we changed some defaults to parse
the XML in a more universal and sane way. So we disabled `normalize` and `trim`
so `xml2js` does not cut out any text content. You can reenable this at will of
course. A more important change is that we return the root tag in the resulting
JavaScript structure via the `explicitRoot` setting, so you need to access the
first element. This is useful for anybody who wants to know what the root node
is and preserves more information. The last major change was to enable
`explicitArray`, so everytime it is possible that one might embed more than one
sub-tag into a tag, xml2js >= 0.2 returns an array even if the array just
includes one element. This is useful when dealing with APIs that return
variable amounts of subtags.
Running tests, development
==========================
[![Build Status](https://travis-ci.org/Leonidas-from-XIV/node-xml2js.svg?branch=master)](https://travis-ci.org/Leonidas-from-XIV/node-xml2js)
[![Coverage Status](https://coveralls.io/repos/Leonidas-from-XIV/node-xml2js/badge.svg?branch=)](https://coveralls.io/r/Leonidas-from-XIV/node-xml2js?branch=master)
[![Dependency Status](https://david-dm.org/Leonidas-from-XIV/node-xml2js.svg)](https://david-dm.org/Leonidas-from-XIV/node-xml2js)
The development requirements are handled by npm, you just need to install them.
We also have a number of unit tests, they can be run using `npm test` directly
from the project root. This runs zap to discover all the tests and execute
them.
If you like to contribute, keep in mind that `xml2js` is written in
CoffeeScript, so don't develop on the JavaScript files that are checked into
the repository for convenience reasons. Also, please write some unit test to
check your behaviour and if it is some user-facing thing, add some
documentation to this README, so people will know it exists. Thanks in advance!
Getting support
===============
Please, if you have a problem with the library, first make sure you read this
README. If you read this far, thanks, you're good. Then, please make sure your
problem really is with `xml2js`. It is? Okay, then I'll look at it. Send me a
mail and we can talk. Please don't open issues, as I don't think that is the
proper forum for support problems. Some problems might as well really be bugs
in `xml2js`, if so I'll let you know to open an issue instead :)
But if you know you really found a bug, feel free to open an issue instead.

View file

@ -0,0 +1,8 @@
"use strict";
export function stripBOM(str) {
if (str[0] === '\uFEFF') {
return str.substring(1);
} else {
return str;
}
};

View file

@ -0,0 +1,118 @@
var escapeCDATA, requiresCDATA, wrapCDATA,
hasProp = {}.hasOwnProperty;
import * as builder from 'xmlbuilder';
import { defaults } from './defaults.js';
requiresCDATA = function(entry) {
return typeof entry === "string" && (entry.indexOf('&') >= 0 || entry.indexOf('>') >= 0 || entry.indexOf('<') >= 0);
};
wrapCDATA = function(entry) {
return "<![CDATA[" + (escapeCDATA(entry)) + "]]>";
};
escapeCDATA = function(entry) {
return entry.replace(']]>', ']]]]><![CDATA[>');
};
export class Builder {
constructor(opts) {
var key, ref, value;
this.options = {};
ref = defaults["0.2"];
for (key in ref) {
if (!hasProp.call(ref, key)) continue;
value = ref[key];
this.options[key] = value;
}
for (key in opts) {
if (!hasProp.call(opts, key)) continue;
value = opts[key];
this.options[key] = value;
}
}
buildObject(rootObj) {
var attrkey, charkey, render, rootElement, rootName;
attrkey = this.options.attrkey;
charkey = this.options.charkey;
if ((Object.keys(rootObj).length === 1) && (this.options.rootName === defaults['0.2'].rootName)) {
rootName = Object.keys(rootObj)[0];
rootObj = rootObj[rootName];
} else {
rootName = this.options.rootName;
}
render = (function(_this) {
return function(element, obj) {
var attr, child, entry, index, key, value;
if (typeof obj !== 'object') {
if (_this.options.cdata && requiresCDATA(obj)) {
element.raw(wrapCDATA(obj));
} else {
element.txt(obj);
}
} else if (Array.isArray(obj)) {
for (index in obj) {
if (!hasProp.call(obj, index)) continue;
child = obj[index];
for (key in child) {
entry = child[key];
element = render(element.ele(key), entry).up();
}
}
} else {
for (key in obj) {
if (!hasProp.call(obj, key)) continue;
child = obj[key];
if (key === attrkey) {
if (typeof child === "object") {
for (attr in child) {
value = child[attr];
element = element.att(attr, value);
}
}
} else if (key === charkey) {
if (_this.options.cdata && requiresCDATA(child)) {
element = element.raw(wrapCDATA(child));
} else {
element = element.txt(child);
}
} else if (Array.isArray(child)) {
for (index in child) {
if (!hasProp.call(child, index)) continue;
entry = child[index];
if (typeof entry === 'string') {
if (_this.options.cdata && requiresCDATA(entry)) {
element = element.ele(key).raw(wrapCDATA(entry)).up();
} else {
element = element.ele(key, entry).up();
}
} else {
element = render(element.ele(key), entry).up();
}
}
} else if (typeof child === "object") {
element = render(element.ele(key), child).up();
} else {
if (typeof child === 'string' && _this.options.cdata && requiresCDATA(child)) {
element = element.ele(key).raw(wrapCDATA(child)).up();
} else {
if (child == null) {
child = '';
}
element = element.ele(key, child.toString()).up();
}
}
}
}
return element;
};
})(this);
rootElement = builder.create(rootName, this.options.xmldec, this.options.doctype, {
headless: this.options.headless,
allowSurrogateChars: this.options.allowSurrogateChars
});
return render(rootElement, rootObj).end(this.options.renderOpts);
};
}

View file

@ -0,0 +1,69 @@
// Generated by CoffeeScript 1.12.7
export const defaults = {
"0.1": {
explicitCharkey: false,
trim: true,
normalize: true,
normalizeTags: false,
attrkey: "@",
charkey: "#",
explicitArray: false,
ignoreAttrs: false,
mergeAttrs: false,
explicitRoot: false,
validator: null,
xmlns: false,
explicitChildren: false,
childkey: '@@',
charsAsChildren: false,
includeWhiteChars: false,
async: false,
strict: true,
attrNameProcessors: null,
attrValueProcessors: null,
tagNameProcessors: null,
valueProcessors: null,
emptyTag: ''
},
"0.2": {
explicitCharkey: false,
trim: false,
normalize: false,
normalizeTags: false,
attrkey: "$",
charkey: "_",
explicitArray: true,
ignoreAttrs: false,
mergeAttrs: false,
explicitRoot: true,
validator: null,
xmlns: false,
explicitChildren: false,
preserveChildrenOrder: false,
childkey: '$$',
charsAsChildren: false,
includeWhiteChars: false,
async: false,
strict: true,
attrNameProcessors: null,
attrValueProcessors: null,
tagNameProcessors: null,
valueProcessors: null,
rootName: 'root',
xmldec: {
'version': '1.0',
'encoding': 'UTF-8',
'standalone': true
},
doctype: null,
renderOpts: {
'pretty': true,
'indent': ' ',
'newline': '\n'
},
headless: false,
chunkSize: 10000,
emptyTag: '',
cdata: false
}
};

View file

@ -0,0 +1,377 @@
var isEmpty, processItem,
bind = function(fn, me){ return function(){ return fn.apply(me, arguments); }; },
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
import sax from 'sax';
import { EventEmitter } from 'events';
import * as bom from './bom.js';
import * as processors from './processors.js';
import { setImmediate } from 'timers';
import { defaults } from './defaults.js';
isEmpty = function(thing) {
return typeof thing === "object" && (thing != null) && Object.keys(thing).length === 0;
};
processItem = function(processors, item, key) {
var i, len, process;
for (i = 0, len = processors.length; i < len; i++) {
process = processors[i];
item = process(item, key);
}
return item;
};
/** @type Class */
export class Parser extends EventEmitter {
// export const Parser = (function(superClass) {
// extend(Parser, superClass);
constructor(opts) {
super();
this.parseStringPromise = bind(this.parseStringPromise, this);
this.parseString = bind(this.parseString, this);
this.reset = bind(this.reset, this);
this.assignOrPush = bind(this.assignOrPush, this);
this.processAsync = bind(this.processAsync, this);
var key, ref, value;
if (!(this instanceof Parser)) {
return new Parser(opts);
}
this.options = {};
ref = defaults["0.2"];
for (key in ref) {
if (!hasProp.call(ref, key)) continue;
value = ref[key];
this.options[key] = value;
}
for (key in opts) {
if (!hasProp.call(opts, key)) continue;
value = opts[key];
this.options[key] = value;
}
if (this.options.xmlns) {
this.options.xmlnskey = this.options.attrkey + "ns";
}
if (this.options.normalizeTags) {
if (!this.options.tagNameProcessors) {
this.options.tagNameProcessors = [];
}
this.options.tagNameProcessors.unshift(processors.normalize);
}
this.reset();
}
processAsync() {
var chunk, err;
try {
if (this.remaining.length <= this.options.chunkSize) {
chunk = this.remaining;
this.remaining = '';
this.saxParser = this.saxParser.write(chunk);
return this.saxParser.close();
} else {
chunk = this.remaining.substr(0, this.options.chunkSize);
this.remaining = this.remaining.substr(this.options.chunkSize, this.remaining.length);
this.saxParser = this.saxParser.write(chunk);
return setImmediate(this.processAsync);
}
} catch (error1) {
err = error1;
if (!this.saxParser.errThrown) {
this.saxParser.errThrown = true;
return this.emit(err);
}
}
};
assignOrPush(obj, key, newValue) {
if (!(key in obj)) {
if (!this.options.explicitArray) {
return obj[key] = newValue;
} else {
return obj[key] = [newValue];
}
} else {
if (!(obj[key] instanceof Array)) {
obj[key] = [obj[key]];
}
return obj[key].push(newValue);
}
};
reset() {
var attrkey, charkey, ontext, stack;
this.removeAllListeners();
this.saxParser = sax.parser(this.options.strict, {
trim: false,
normalize: false,
xmlns: this.options.xmlns
});
this.saxParser.errThrown = false;
this.saxParser.onerror = (function(_this) {
return function(error) {
_this.saxParser.resume();
if (!_this.saxParser.errThrown) {
_this.saxParser.errThrown = true;
return _this.emit("error", error);
}
};
})(this);
this.saxParser.onend = (function(_this) {
return function() {
if (!_this.saxParser.ended) {
_this.saxParser.ended = true;
return _this.emit("end", _this.resultObject);
}
};
})(this);
this.saxParser.ended = false;
this.EXPLICIT_CHARKEY = this.options.explicitCharkey;
this.resultObject = null;
stack = [];
attrkey = this.options.attrkey;
charkey = this.options.charkey;
this.saxParser.onopentag = (function(_this) {
return function(node) {
var key, newValue, obj, processedKey, ref;
obj = Object.create(null);
obj[charkey] = "";
if (!_this.options.ignoreAttrs) {
ref = node.attributes;
for (key in ref) {
if (!hasProp.call(ref, key)) continue;
if (!(attrkey in obj) && !_this.options.mergeAttrs) {
obj[attrkey] = Object.create(null);
}
newValue = _this.options.attrValueProcessors ? processItem(_this.options.attrValueProcessors, node.attributes[key], key) : node.attributes[key];
processedKey = _this.options.attrNameProcessors ? processItem(_this.options.attrNameProcessors, key) : key;
if (_this.options.mergeAttrs) {
_this.assignOrPush(obj, processedKey, newValue);
} else {
obj[attrkey][processedKey] = newValue;
}
}
}
obj["#name"] = _this.options.tagNameProcessors ? processItem(_this.options.tagNameProcessors, node.name) : node.name;
if (_this.options.xmlns) {
obj[_this.options.xmlnskey] = {
uri: node.uri,
local: node.local
};
}
return stack.push(obj);
};
})(this);
this.saxParser.onclosetag = (function(_this) {
return function() {
var cdata, emptyStr, key, node, nodeName, obj, objClone, old, s, xpath;
obj = stack.pop();
nodeName = obj["#name"];
if (!_this.options.explicitChildren || !_this.options.preserveChildrenOrder) {
delete obj["#name"];
}
if (obj.cdata === true) {
cdata = obj.cdata;
delete obj.cdata;
}
s = stack[stack.length - 1];
if (obj[charkey].match(/^\s*$/) && !cdata && !_this.options.includeWhiteChars) {
emptyStr = obj[charkey];
delete obj[charkey];
} else {
if (_this.options.trim) {
obj[charkey] = obj[charkey].trim();
}
if (_this.options.normalize) {
obj[charkey] = obj[charkey].replace(/\s{2,}/g, " ").trim();
}
obj[charkey] = _this.options.valueProcessors ? processItem(_this.options.valueProcessors, obj[charkey], nodeName) : obj[charkey];
if (Object.keys(obj).length === 1 && charkey in obj && !_this.EXPLICIT_CHARKEY) {
obj = obj[charkey];
}
}
if (isEmpty(obj)) {
if (typeof _this.options.emptyTag === 'function') {
obj = _this.options.emptyTag();
} else {
obj = _this.options.emptyTag !== '' ? _this.options.emptyTag : emptyStr;
}
}
if (_this.options.validator != null) {
xpath = "/" + ((function() {
var i, len, results;
results = [];
for (i = 0, len = stack.length; i < len; i++) {
node = stack[i];
results.push(node["#name"]);
}
return results;
})()).concat(nodeName).join("/");
(function() {
var err;
try {
return obj = _this.options.validator(xpath, s && s[nodeName], obj);
} catch (error1) {
err = error1;
return _this.emit("error", err);
}
})();
}
if (_this.options.explicitChildren && !_this.options.mergeAttrs && typeof obj === 'object') {
if (!_this.options.preserveChildrenOrder) {
node = Object.create(null);
if (_this.options.attrkey in obj) {
node[_this.options.attrkey] = obj[_this.options.attrkey];
delete obj[_this.options.attrkey];
}
if (!_this.options.charsAsChildren && _this.options.charkey in obj) {
node[_this.options.charkey] = obj[_this.options.charkey];
delete obj[_this.options.charkey];
}
if (Object.getOwnPropertyNames(obj).length > 0) {
node[_this.options.childkey] = obj;
}
obj = node;
} else if (s) {
s[_this.options.childkey] = s[_this.options.childkey] || [];
objClone = Object.create(null);
for (key in obj) {
if (!hasProp.call(obj, key)) continue;
objClone[key] = obj[key];
}
s[_this.options.childkey].push(objClone);
delete obj["#name"];
if (Object.keys(obj).length === 1 && charkey in obj && !_this.EXPLICIT_CHARKEY) {
obj = obj[charkey];
}
}
}
if (stack.length > 0) {
return _this.assignOrPush(s, nodeName, obj);
} else {
if (_this.options.explicitRoot) {
old = obj;
obj = Object.create(null);
obj[nodeName] = old;
}
_this.resultObject = obj;
_this.saxParser.ended = true;
return _this.emit("end", _this.resultObject);
}
};
})(this);
ontext = (function(_this) {
return function(text) {
var charChild, s;
s = stack[stack.length - 1];
if (s) {
s[charkey] += text;
if (_this.options.explicitChildren && _this.options.preserveChildrenOrder && _this.options.charsAsChildren && (_this.options.includeWhiteChars || text.replace(/\\n/g, '').trim() !== '')) {
s[_this.options.childkey] = s[_this.options.childkey] || [];
charChild = {
'#name': '__text__'
};
charChild[charkey] = text;
if (_this.options.normalize) {
charChild[charkey] = charChild[charkey].replace(/\s{2,}/g, " ").trim();
}
s[_this.options.childkey].push(charChild);
}
return s;
}
};
})(this);
this.saxParser.ontext = ontext;
return this.saxParser.oncdata = (function(_this) {
return function(text) {
var s;
s = ontext(text);
if (s) {
return s.cdata = true;
}
};
})(this);
};
parseString(str, cb) {
var err;
if ((cb != null) && typeof cb === "function") {
this.on("end", function(result) {
this.reset();
return cb(null, result);
});
this.on("error", function(err) {
this.reset();
return cb(err);
});
}
try {
str = str.toString();
if (str.trim() === '') {
this.emit("end", null);
return true;
}
str = bom.stripBOM(str);
if (this.options.async) {
this.remaining = str;
setImmediate(this.processAsync);
return this.saxParser;
}
return this.saxParser.write(str).close();
} catch (error1) {
err = error1;
if (!(this.saxParser.errThrown || this.saxParser.ended)) {
this.emit('error', err);
return this.saxParser.errThrown = true;
} else if (this.saxParser.ended) {
throw err;
}
}
};
parseStringPromise(str) {
return new Promise((function(_this) {
return function(resolve, reject) {
return _this.parseString(str, function(err, value) {
if (err) {
return reject(err);
} else {
return resolve(value);
}
});
};
})(this));
};
}
export const parseString = function(str, a, b) {
var cb, options, parser;
if (b != null) {
if (typeof b === 'function') {
cb = b;
}
if (typeof a === 'object') {
options = a;
}
} else {
if (typeof a === 'function') {
cb = a;
}
options = {};
}
parser = new Parser(options);
return parser.parseString(str, cb);
};
export const parseStringPromise = function(str, a) {
var options, parser;
if (typeof a === 'object') {
options = a;
}
parser = new Parser(options);
return parser.parseStringPromise(str);
};

View file

@ -0,0 +1,31 @@
"use strict";
var prefixMatch;
prefixMatch = new RegExp(/(?!xmlns)^.*:/);
export const normalize = function(str) {
return str.toLowerCase();
};
export const firstCharLowerCase = function(str) {
return str.charAt(0).toLowerCase() + str.slice(1);
};
export const stripPrefix = function(str) {
return str.replace(prefixMatch, '');
};
export const parseNumbers = function(str) {
if (!isNaN(str)) {
str = str % 1 === 0 ? parseInt(str, 10) : parseFloat(str);
}
return str;
};
export const parseBooleans = function(str) {
if (/^(?:true|false)$/i.test(str)) {
str = str.toLowerCase() === 'true';
}
return str;
};

View file

@ -0,0 +1,27 @@
var
extend = function(child, parent) { for (var key in parent) { if (hasProp.call(parent, key)) child[key] = parent[key]; } function ctor() { this.constructor = child; } ctor.prototype = parent.prototype; child.prototype = new ctor(); child.__super__ = parent.prototype; return child; },
hasProp = {}.hasOwnProperty;
// import { defaults } from './defaults.js';
import * as builder from './builder.js';
import * as parser from './parser.js';
import * as processors from './processors.js';
// export const defaults = defaults.defaults;
// export const processors = processors;
/** @type Class */
export class ValidationError extends Error {
constructor(message) {
super(message);
this.message = message;
}
};
export const Parser = parser.Parser;
export const Builder = builder.Builder;
export const parseString = parser.parseString;
export const parseStringPromise = parser.parseStringPromise;

View file

@ -1,4 +1,4 @@
import * as xml2js from 'xml2js';
import * as xml2js from '../deps/xml2js/xml2js.js';
import { KPJFile, KPJFileProject } from './kpj-file.js';
import { boxXmlArray } from '../util/util.js';
import { KeymanDeveloperProject, KeymanDeveloperProjectFile10, KeymanDeveloperProjectType } from './keyman-developer-project.js';

View file

@ -1,4 +1,4 @@
import * as xml2js from 'xml2js';
import * as xml2js from '../deps/xml2js/xml2js.js';
import KVKSourceFile from './kvks-file.js';
import { boxXmlArray } from '../util/util.js';
import { DEFAULT_KVK_FONT, VisualKeyboard, VisualKeyboardHeaderFlags, VisualKeyboardKey, VisualKeyboardKeyFlags, VisualKeyboardLegalShiftStates, VisualKeyboardShiftState } from './visual-keyboard.js';

View file

@ -1,4 +1,4 @@
import * as xml2js from 'xml2js';
import * as xml2js from '../deps/xml2js/xml2js.js';
import KVKSourceFile, { KVKSEncoding, KVKSFlags, KVKSKey, KVKSLayer } from './kvks-file.js';
import { VisualKeyboard, VisualKeyboardHeaderFlags, VisualKeyboardKeyFlags, VisualKeyboardLegalShiftStates, VisualKeyboardShiftState } from './visual-keyboard.js';
import { USVirtualKeyCodes } from '../consts/virtual-key-constants.js';

View file

@ -1,4 +1,4 @@
import * as xml2js from 'xml2js';
import * as xml2js from '../deps/xml2js/xml2js.js';
import { LDMLKeyboardXMLSourceFile, LKImport, ImportStatus } from './ldml-keyboard-xml.js';
import { boxXmlArray } from '../util/util.js';
import { CompilerCallbacks } from '../util/compiler-interfaces.js';

View file

@ -55,4 +55,6 @@ export * as util from './util/util.js';
export * as KeymanFileTypes from './util/file-types.js';
export * as Schemas from './schemas.js';
export * as SchemaValidators from './schema-validators.js';
export * as SchemaValidators from './schema-validators.js';
export * as xml2js from './deps/xml2js/xml2js.js';

View file

@ -9,6 +9,7 @@
"preserveConstEnums": true,
},
"include": [
"src/deps/xml2js/*.js",
"src/**/*.ts",
"src/schemas/*.mjs", // Import the validators
],

View file

@ -29,7 +29,7 @@
"c8": "^7.12.0",
"mocha": "^10.0.0",
"mocha-teamcity-reporter": "^4.0.0",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"type": "module",
"paths": {

View file

@ -13,7 +13,7 @@ project('keyman_core', 'cpp', 'c',
'b_vscrt=static_from_buildtype',
'warning_level=2',
'debug=true'],
meson_version: '>=0.57.0')
meson_version: '>=1.0')
# Import our standard compiler defines; this is copied from
# /resources/build/standard.meson.build by build.sh, because

View file

@ -228,8 +228,10 @@ bool has_nfd_boundary_before(km_core_usv cp) {
}
/**
* Helper to convert icu::UnicodeString to a UTF-32 km_core_usv buffer,
* nul-terminated
* Helper to convert std::u32string to a UTF-32 km_core_usv buffer,
* nul-terminated.
* Parallel to unicode_string_to_usv()
* @returns new buffer, caller owns storage
*/
km_core_usv *string_to_usv(const std::u32string& src) {
return km::core::kmx::u32dup(src.c_str());

View file

@ -55,7 +55,7 @@ write_nfd_table() {
// collect the raw list of chars that do NOT have a boundary before them.
std::vector<km_core_usv> noBoundary;
for (km_core_usv ch = 0; ch < 0x10FFFF; ch++) {
for (km_core_usv ch = 0; ch < km::core::kmx::Uni_MAX_CODEPOINT; ch++) {
bool bb = nfd->hasBoundaryBefore(ch);
assert(!(ch == 0 && !bb)); // assert that we can use U+0000 as a terminator
if (bb) continue; //only emit nonboundary

View file

@ -21,7 +21,7 @@ if get_option('keyman_core_tests')
if get_option('default_library') != 'static'
ctypes_void_p_size = ['-c', 'import ctypes; print(ctypes.sizeof(ctypes.c_void_p))']
r = run_command(python, ctypes_void_p_size)
r = run_command(python, ctypes_void_p_size, check: true)
python_ctypes_compatible = r.stdout().to_int() == cpp_compiler.sizeof('void *')
if not python_ctypes_compatible
message('Python ctypes is incompatible with built shared object. Disabling some tests.')

View file

@ -0,0 +1,4 @@
@echo off
set infile=%1
set infileb=%infile:/=\%
type %infileb%

View file

@ -17,9 +17,9 @@ binary_tests = [
foreach kbd : binary_tests
configure_file(
command: copy_cmd + ['@INPUT@', '@OUTPUT@'],
input: kbd + '.kmn',
output: kbd + '.kmn'
output: kbd + '.kmn',
copy: true
)
configure_file(

View file

@ -89,10 +89,8 @@ kmc_root = meson.current_source_dir() / '../../../../developer/src/kmc/build/src
kmc_cmd = [node, '--enable-source-maps', kmc_root]
if build_machine.system() == 'windows'
copy_cmd = [find_program('cmd.exe', required: true), '/c', 'copy']
cat_cmd = [find_program('cmd.exe', required: true), '/c', 'type']
cat_cmd = [find_program(meson.current_source_dir() / 'cat.bat', required: true)]
else
copy_cmd = [find_program('cp', required: true)]
cat_cmd = [find_program('cat', required: true)]
endif
@ -127,7 +125,7 @@ foreach kbd : tests
kbd_src_path = common_test_keyboards_baseline / kbd + '.kmn'
content = run_command(
cat_cmd, files(kbd_src_path),
cat_cmd, files(kbd_src_path), check: true,
).stdout().strip()
cfg = configuration_data()

View file

@ -8,19 +8,13 @@ invalid_tests = [
'ik_000_null_invalid'
]
if build_machine.system() == 'windows'
copy_cmd = [find_program('cmd.exe', required: true), '/c', 'copy']
else
copy_cmd = [find_program('cp', required: true)]
endif
# Build all keyboards in output folder
foreach kbd : invalid_tests
configure_file(
command: copy_cmd + ['@INPUT@', '@OUTPUT@'],
input: kbd + '.xml',
output: kbd + '.xml'
output: kbd + '.xml',
copy: true
)
configure_file(

View file

@ -56,8 +56,8 @@ tests += tests_from_cldr
# Setup kmc
kmc_root = join_paths(meson.source_root(),'..','developer','src','kmc')
ldml_root = join_paths(meson.source_root(),'..','resources','standards-data','ldml-keyboards','45')
kmc_root = meson.global_source_root() / '../developer/src/kmc'
ldml_root = meson.global_source_root() / '../resources/standards-data/ldml-keyboards/45'
ldml_data = join_paths(ldml_root, '3.0')
ldml_testdata = join_paths(ldml_root, 'test')
kmc_cmd = [node, '--enable-source-maps', kmc_root]

View file

@ -22,12 +22,6 @@ invalid_tests = []
# Setup copying of source files, used in child subdir calls
if build_machine.system() == 'windows'
copy_cmd = [find_program('cmd.exe', required: true), '/c', 'copy']
else
copy_cmd = [find_program('cp', required: true)]
endif
if node.found()
# Note: if node is not available, we cannot build the keyboards; build.sh
# emits a warning that the ldml keyboard tests will be skipped

View file

@ -2,10 +2,10 @@ node = find_program('node', required: true)
common_test_files = [
meson.current_source_dir() / 'emscripten_filesystem.cpp',
meson.source_root() / '../common/include/test_color.cpp'
meson.global_source_root() / '../common/include/test_color.cpp'
]
hextobin_root = join_paths(meson.source_root(),'..','common','tools','hextobin','build','hextobin.js')
hextobin_root = meson.global_source_root() / '../common/tools/hextobin/build/hextobin.js'
hextobin_cmd = [node, hextobin_root]
subdir('json')

View file

@ -227,7 +227,15 @@ begin
// Look for Keyboard_<id>
with TStringStream.Create('', TEncoding.UTF8) do
try
LoadFromFile(f.FileName);
try
LoadFromFile(f.FileName);
except
on E:EEncodingError do
begin
// If the file cannot be loaded as UTF-8, we'll ignore it; #11687
Exit(False);
end;
end;
Result := TRegEx.IsMatch(DataString, '\bKeyboard_'+id+'\b', []);
finally
Free;

View file

@ -10,8 +10,7 @@
],
"devDependencies": {
"@types/node": "^20.4.1",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"scripts": {
"build": "tsc -b"

View file

@ -2,4 +2,5 @@ export { validateMITLicense } from './src/validate-mit-license.js';
export { KeymanSentry, SentryNodeOptions } from './src/KeymanSentry.js';
export { getOption, loadOptions, clearOptions } from './src/options.js';
export { escapeMarkdownChar } from './src/markdown.js';
export { KeymanUrls } from './src/keyman-urls.js';
export { KeymanUrls } from './src/keyman-urls.js';
export { isValidEmail } from './src/is-valid-email.js';

View file

@ -13,8 +13,7 @@
},
"devDependencies": {
"@types/node": "^20.4.1",
"ts-node": "^9.1.1",
"typescript": "^4.9.5",
"typescript": "^5.4.5",
"c8": "^7.12.0",
"mocha": "^8.4.0"
},

View file

@ -0,0 +1,18 @@
/*
* Keyman is copyright (C) SIL International. MIT License.
*
* Verify email address format, following WHATWG guidelines
*/
// There is no "good" definition of a valid email address. Email addresses are
// horrific. They can contain comments, whitespace, and all manner of ugly
// things. Because we use AJV to verify JSON files, we use their specification
// on what is a valid email address. Some useful references:
// * https://html.spec.whatwg.org/multipage/input.html#valid-e-mail-address
// * http://stackoverflow.com/questions/201323/using-a-regular-expression-to-validate-an-email-address#answer-8829363
// * https://github.com/ajv-validator/ajv-formats/blob/4ca86d21bd07571a30178cbb3714133db6eada9a/src/formats.ts#L122
// * https://github.com/ajv-validator/ajv-formats/blob/4ca86d21bd07571a30178cbb3714133db6eada9a/src/formats.ts#L65
export function isValidEmail(email: string) {
return /^[a-z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i.test(email);
}

View file

@ -0,0 +1,35 @@
import { assert } from 'chai';
import 'mocha';
import { isValidEmail } from '../src/is-valid-email.js';
describe('test-is-valid-email', function () {
it('should accept a valid email address', function() {
[
'email@example.com',
'email.example-true@example.com',
'email@example-example.com',
'email...@example.com',
'email_example1@1example.com',
'email+example@mail.example.com',
'Email@Example.Com'
].forEach(email => assert.isTrue(isValidEmail(email), `expected '${email}' to be valid`));
// This is accepted, but it's really a bit wonky. But that's an upstream
// issue with overly lax regex and not something we'll attempt to fix:
//
// assert.isTrue(isValidEmail('.@example.com'));
});
it('should reject invalid email addresses', function() {
[
'email@example.com, email2@example.com',
'<Mr Email> email@example.com',
'email@example_domain.com',
'email',
'email@.',
'email@example..com',
'email@',
'@example',
].forEach(email => assert.isFalse(isValidEmail(email), `expected '${email}' to be invalid`));
});
});

View file

@ -34,8 +34,7 @@
"c8": "^7.12.0",
"chalk": "^2.4.2",
"mocha": "^8.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"repository": {
"type": "git",

View file

@ -33,8 +33,7 @@
"c8": "^7.12.0",
"chalk": "^2.4.2",
"mocha": "^8.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"mocha": {
"spec": "build/test/**/test-*.js",

View file

@ -8,7 +8,7 @@ import { KeyboardInfoFile, KeyboardInfoFileIncludes, KeyboardInfoFileLanguageFon
import { KeymanFileTypes, CompilerCallbacks, KmpJsonFile, KmxFileReader, KMX, KeymanTargets, KeymanCompiler, CompilerOptions, KeymanCompilerResult, KeymanCompilerArtifacts, KeymanCompilerArtifact } from "@keymanapp/common-types";
import { KeyboardInfoCompilerMessages } from "./keyboard-info-compiler-messages.js";
import langtags from "./imports/langtags.js";
import { KeymanUrls, validateMITLicense } from "@keymanapp/developer-utils";
import { KeymanUrls, isValidEmail, validateMITLicense } from "@keymanapp/developer-utils";
import { KmpCompiler } from "@keymanapp/kmc-package";
import { SchemaValidators } from "@keymanapp/common-types";
@ -238,6 +238,11 @@ export class KeyboardInfoCompiler implements KeymanCompiler {
return null;
}
if(!isValidEmail(match[2])) {
this.callbacks.reportMessage(KeyboardInfoCompilerMessages.Error_InvalidAuthorEmail({email:author.url}));
return null;
}
keyboard_info.authorEmail = match[2];
}
}

View file

@ -0,0 +1,21 @@
The MIT License (MIT)
Copyright (c) 2015-2022 SIL International
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.

View file

@ -0,0 +1 @@
khmer_angkor.js -text

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,187 @@
<?xml version="1.0" encoding="utf-8"?>
<KeymanDeveloperProject>
<Options>
<BuildPath>$PROJECTPATH\build</BuildPath>
<CompilerWarningsAsErrors>True</CompilerWarningsAsErrors>
<WarnDeprecatedCode>True</WarnDeprecatedCode>
<CheckFilenameConventions>False</CheckFilenameConventions>
<ProjectType>keyboard</ProjectType>
</Options>
<Files>
<File>
<ID>id_f347675c33d2e6b1c705c787fad4941a</ID>
<Filename>khmer_angkor.kmn</Filename>
<Filepath>source\khmer_angkor.kmn</Filepath>
<FileVersion>1.3</FileVersion>
<FileType>.kmn</FileType>
<Details>
<Name>Khmer Angkor</Name>
<Copyright>© 2015-2022 SIL International</Copyright>
<Message>More than just a Khmer Unicode keyboard.</Message>
</Details>
</File>
<File>
<ID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ID>
<Filename>khmer_angkor.kps</Filename>
<Filepath>source\khmer_angkor.kps</Filepath>
<FileVersion></FileVersion>
<FileType>.kps</FileType>
<Details>
<Name>Khmer Angkor</Name>
<Copyright>© 2015-2022 SIL International</Copyright>
</Details>
</File>
<File>
<ID>id_8a1efc7c4ab7cfece8aedd847679ca27</ID>
<Filename>khmer_angkor.ico</Filename>
<Filepath>source\khmer_angkor.ico</Filepath>
<FileVersion></FileVersion>
<FileType>.ico</FileType>
<ParentFileID>id_f347675c33d2e6b1c705c787fad4941a</ParentFileID>
</File>
<File>
<ID>id_8dc195db32d1fd0514de0ad51fff5df0</ID>
<Filename>khmer_angkor.js</Filename>
<Filepath>source\..\build\khmer_angkor.js</Filepath>
<FileVersion></FileVersion>
<FileType>.js</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_10596632fcbf4138d24bcccf53e6ae01</ID>
<Filename>khmer_angkor.kvk</Filename>
<Filepath>source\..\build\khmer_angkor.kvk</Filepath>
<FileVersion></FileVersion>
<FileType>.kvk</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_0a851f95ce553ecd62cbee6c32ced68f</ID>
<Filename>khmer_angkor.kmx</Filename>
<Filepath>source\..\build\khmer_angkor.kmx</Filepath>
<FileVersion></FileVersion>
<FileType>.kmx</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_d8b6eb05f4b7e2945c10e04c1f49e4c8</ID>
<Filename>keyboard_layout.png</Filename>
<Filepath>source\welcome\keyboard_layout.png</Filepath>
<FileVersion></FileVersion>
<FileType>.png</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_724e5b4c63f10bc0abf7077f7c3172fc</ID>
<Filename>welcome.htm</Filename>
<Filepath>source\welcome\welcome.htm</Filepath>
<FileVersion></FileVersion>
<FileType>.htm</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_35857cb2b54f123612735ec948400082</ID>
<Filename>FONTLOG.txt</Filename>
<Filepath>source\..\..\..\shared\fonts\khmer\mondulkiri\FONTLOG.txt</Filepath>
<FileVersion></FileVersion>
<FileType>.txt</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_7e3afe5bb59b888b08b48cd5817d8de4</ID>
<Filename>Mondulkiri-B.ttf</Filename>
<Filepath>source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-B.ttf</Filepath>
<FileVersion></FileVersion>
<FileType>.ttf</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_b9734e80f86c69ea5ae4dfa9f0083d09</ID>
<Filename>Mondulkiri-BI.ttf</Filename>
<Filepath>source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-BI.ttf</Filepath>
<FileVersion></FileVersion>
<FileType>.ttf</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_25abe4d2b0abc03a5be5b666a8de776e</ID>
<Filename>Mondulkiri-I.ttf</Filename>
<Filepath>source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-I.ttf</Filepath>
<FileVersion></FileVersion>
<FileType>.ttf</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_b766568498108eee46ed1601ff69c47d</ID>
<Filename>Mondulkiri-R.ttf</Filename>
<Filepath>source\..\..\..\shared\fonts\khmer\mondulkiri\Mondulkiri-R.ttf</Filepath>
<FileVersion></FileVersion>
<FileType>.ttf</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_84544d04133cab3dbfc86b91ad1a4e17</ID>
<Filename>OFL.txt</Filename>
<Filepath>source\..\..\..\shared\fonts\khmer\mondulkiri\OFL.txt</Filepath>
<FileVersion></FileVersion>
<FileType>.txt</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_0c33fbefd1c20f487b1bea2343b3bb2c</ID>
<Filename>OFL-FAQ.txt</Filename>
<Filepath>source\..\..\..\shared\fonts\khmer\mondulkiri\OFL-FAQ.txt</Filepath>
<FileVersion></FileVersion>
<FileType>.txt</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_a59d89fca36a310147645fa2604e521b</ID>
<Filename>KAK_Documentation_EN.pdf</Filename>
<Filepath>source\welcome\KAK_Documentation_EN.pdf</Filepath>
<FileVersion></FileVersion>
<FileType>.pdf</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_5643c4cd3933b3ada0b4af6579305ec4</ID>
<Filename>KAK_Documentation_KH.pdf</Filename>
<Filepath>source\welcome\KAK_Documentation_KH.pdf</Filepath>
<FileVersion></FileVersion>
<FileType>.pdf</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_8da344c4cea6f467013357fe099006f5</ID>
<Filename>readme.htm</Filename>
<Filepath>source\readme.htm</Filepath>
<FileVersion></FileVersion>
<FileType>.htm</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_acb0dd94c60e345d999670e999cbd159</ID>
<Filename>image002.png</Filename>
<Filepath>source\welcome\image002.png</Filepath>
<FileVersion></FileVersion>
<FileType>.png</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_4edf70bc019f05b5ad39a2ea727ad547</ID>
<Filename>khmer_busra_kbd.ttf</Filename>
<Filepath>source\..\..\..\shared\fonts\khmer\busrakbd\khmer_busra_kbd.ttf</Filepath>
<FileVersion></FileVersion>
<FileType>.ttf</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
<File>
<ID>id_bc823844e4399751e1867016801f7327</ID>
<Filename>splash.gif</Filename>
<Filepath>source\splash.gif</Filepath>
<FileVersion></FileVersion>
<FileType>.gif</FileType>
<ParentFileID>id_8d4eb765f80c9f2b0f769cf4e4aaa456</ParentFileID>
</File>
</Files>
</KeymanDeveloperProject>

View file

@ -0,0 +1,49 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>15.0.266.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Options>
<LicenseFile>..\LICENSE.md</LicenseFile>
<FollowKeyboardVersion/>
</Options>
<Info>
<Name URL="">Khmer Angkor</Name>
<Copyright URL="">© 2015-2022 SIL International</Copyright>
<Author URL="mailto:makara_sok@sil.org, example@example.com">Multiple Authors</Author>
<Version URL=""></Version>
<WebSite URL="https://keyman.com/keyboards/khmer_angkor">https://keyman.com/keyboards/khmer_angkor</WebSite>
<Description URL="">Khmer Unicode keyboard layout based on the NiDA keyboard layout. Automatically corrects many common keying errors.</Description>
</Info>
<Files>
<File>
<Name>..\LICENSE.md</Name>
<Description>File LICENSE.md</Description>
<CopyLocation>0</CopyLocation>
<FileType>.md</FileType>
</File>
<File>
<Name>..\build\khmer_angkor.kmx</Name>
<Description>Keyboard Khmer Angkor</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
</File>
<File>
<Name>..\build\khmer_angkor.js</Name>
<Description>Keyboard Khmer Angkor</Description>
<CopyLocation>0</CopyLocation>
<FileType>.js</FileType>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Khmer Angkor</Name>
<ID>khmer_angkor</ID>
<Version>1.3</Version>
<Languages>
<Language ID="km">Central Khmer (Khmer, Cambodia)</Language>
</Languages>
</Keyboard>
</Keyboards>
</Package>

View file

@ -56,7 +56,7 @@ describe('KeyboardInfoCompilerMessages', function () {
assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_FileDoesNotExist).includes(KeymanFileTypes.Binary.WebKeyboard),
KeymanFileTypes.Binary.WebKeyboard+' not found in the message');
});
// ERROR_FileDoesNotExist (.kmp fileSize)
it('should generate ERROR_FileDoesNotExist error if .kmp file does not exist', async function() {
@ -141,7 +141,7 @@ describe('KeyboardInfoCompilerMessages', function () {
assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_LicenseFileIsMissing).includes(licenseFilename),
licenseFilename+' not found in the message');
});
// ERROR_LicenseFileIsDamaged (error on decode)
it('should generate ERROR_LicenseFileIsDamaged error if license file throws error on decode', async function() {
@ -197,7 +197,7 @@ describe('KeyboardInfoCompilerMessages', function () {
assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_LicenseIsNotValid).includes(licenseFilename),
licenseFilename+' not found in the message');
});
// ERROR_CannotBuildWithoutKmpFile
it('should generate ERROR_CannotBuildWithoutKmpFile error if .kmp file is not in sources', async function() {
@ -227,7 +227,7 @@ describe('KeyboardInfoCompilerMessages', function () {
assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_CannotBuildWithoutKmpFile),
`ERROR_CannotBuildWithoutKmpFile not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
});
// ERROR_NoLicenseFound
it('should generate ERROR_NoLicenseFound error if licence file is not in .kps options', async function() {
@ -257,7 +257,7 @@ describe('KeyboardInfoCompilerMessages', function () {
assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_NoLicenseFound),
`ERROR_NoLicenseFound not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
});
// ERROR_FontFileMetaDataIsInvalid
it('should generate ERROR_FontFileMetaDataIsInvalid error if font file meta data throws an error', async function() {
@ -285,7 +285,38 @@ describe('KeyboardInfoCompilerMessages', function () {
`ERROR_FontFileMetaDataIsInvalid not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
assert.isTrue(nodeCompilerMessage(callbacks, KeyboardInfoCompilerMessages.ERROR_FontFileMetaDataIsInvalid).includes(kmpJsonData.files[0].name),
kmpJsonData.files[0].name+' not found in the message');
});
});
// ERROR_InvalidAuthorEmail
it('should generate ERROR_InvalidAuthorEmail error if multiple email addresses are listed in .kps', async function() {
const jsFilename = makePathToFixture('multiple-email-addresses', 'build', 'khmer_angkor.js');
const kpsFilename = makePathToFixture('multiple-email-addresses', 'source', 'khmer_angkor.kps');
const kmpFilename = makePathToFixture('multiple-email-addresses', 'build', 'khmer_angkor.kmp');
const sources = {
kmpFilename,
sourcePath: 'release/k/multiple-email-addresses',
kpsFilename,
jsFilename: jsFilename,
forPublishing: true,
};
const compiler = new KeyboardInfoCompiler();
assert.isTrue(await compiler.init(callbacks, {sources}));
let result: KeyboardInfoCompilerResult = null;
try {
result = await compiler.run(kmpFilename, null);
} catch(e) {
callbacks.printMessages();
throw e;
}
assert.isNull(result);
assert.isTrue(callbacks.hasMessage(KeyboardInfoCompilerMessages.ERROR_InvalidAuthorEmail),
`ERROR_InvalidAuthorEmail not generated, instead got: `+JSON.stringify(callbacks.messages,null,2));
});
});
function nodeCompilerMessage(ncb: TestCompilerCallbacks, code: number): string {

View file

@ -41,8 +41,7 @@
"chalk": "^2.4.2",
"mocha": "^8.4.0",
"sinon-chai": "^3.7.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"mocha": {
"spec": "build/test/**/test-*.js",

View file

@ -28,7 +28,6 @@
"@keymanapp/keyman-version": "*",
"@keymanapp/kmc-kmn": "*",
"@keymanapp/ldml-keyboard-constants": "*",
"restructure": "git+https://github.com/keymanapp/dependency-restructure.git#7a188a1e26f8f36a175d95b67ffece8702363dfc",
"semver": "^7.5.2"
},
"devDependencies": {
@ -40,8 +39,7 @@
"c8": "^7.12.0",
"chalk": "^2.4.2",
"mocha": "^8.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"mocha": {
"spec": "build/test/**/test-*.js",

View file

@ -40,8 +40,7 @@
"c8": "^7.12.0",
"chalk": "^2.4.2",
"mocha": "^8.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"mocha": {
"spec": "build/test/**/test-*.js",

View file

@ -7,7 +7,7 @@ import { minKeymanVersion } from "./min-keyman-version.js";
import { ModelInfoFile } from "./model-info-file.js";
import { CompilerCallbacks, CompilerOptions, KeymanCompiler, KeymanCompilerArtifact, KeymanCompilerArtifacts, KeymanCompilerResult, KmpJsonFile } from "@keymanapp/common-types";
import { ModelInfoCompilerMessages } from "./model-info-compiler-messages.js";
import { KeymanUrls, validateMITLicense } from "@keymanapp/developer-utils";
import { KeymanUrls, isValidEmail, validateMITLicense } from "@keymanapp/developer-utils";
/* c8 ignore start */
/**
@ -192,6 +192,11 @@ export class ModelInfoCompiler implements KeymanCompiler {
return null;
}
if(!isValidEmail(match[2])) {
this.callbacks.reportMessage(ModelInfoCompilerMessages.Error_InvalidAuthorEmail({email:author.url}));
return null;
}
model_info.authorEmail = match[2];
}

View file

@ -33,6 +33,7 @@ builder_parse "$@"
#-------------------------------------------------------------------------------------------------------------------
function do_build() {
tsc -b ./tools/tsconfig.json
npm run build
}

View file

@ -33,7 +33,7 @@
"@keymanapp/common-types": "*",
"@keymanapp/keyman-version": "*",
"@keymanapp/models-types": "*",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"devDependencies": {
"@keymanapp/developer-test-helpers": "*",
@ -43,8 +43,7 @@
"c8": "^7.12.0",
"chalk": "^2.4.2",
"esbuild": "^0.15.7",
"mocha": "^10.0.0",
"ts-node": "^10.9.1"
"mocha": "^10.0.0"
},
"mocha": {
"spec": "build/test/**/test-*.js",

View file

@ -1,5 +1,3 @@
#!/usr/bin/env npx ts-node
/**
* Prints a JavaScript regular expression suitable for use in the
* **overrideScriptDefaults** word breaker decorator.
@ -67,7 +65,7 @@ export const HAS_SOUTHEAST_ASIAN_LETTER = /[${characterClasses}]/;`);
////////////////////////////////// Helpers ///////////////////////////////////
function* unicodeData() {
let unicodeDataFile = readFileSync(path.join(UCD_DIR, "UnicodeData.txt"), "UTF-8");
let unicodeDataFile = readFileSync(path.join(UCD_DIR, "UnicodeData.txt"), "utf8");
for (let line of unicodeDataFile.split("\n")) {
if (line.trim() == "") {
continue;
@ -82,7 +80,7 @@ function* unicodeData() {
}
function* blocks() {
let blocksFile = readFileSync(path.join(UCD_DIR, "Blocks.txt"), "UTF-8");
let blocksFile = readFileSync(path.join(UCD_DIR, "Blocks.txt"), "utf8");
for (let line of blocksFile.split("\n")) {
if (line.trim() === "") {
continue;

View file

@ -0,0 +1,13 @@
{
"extends": "../tsconfig.json",
"compilerOptions": {
"outDir": "../build/tools/",
"rootDir": ".",
"baseUrl": "../.",
"allowSyntheticDefaultImports": true,
},
"include": [
"./*.ts"
]
}

View file

@ -31,8 +31,7 @@
"dependencies": {
"@keymanapp/common-types": "*",
"jszip": "^3.7.0",
"marked": "^7.0.0",
"xml2js": "git+https://github.com/keymanapp/dependency-node-xml2js#535fe732dc408d697e0f847c944cc45f0baf0829"
"marked": "^7.0.0"
},
"devDependencies": {
"@keymanapp/developer-test-helpers": "*",
@ -41,8 +40,7 @@
"c8": "^7.12.0",
"chalk": "^2.4.2",
"mocha": "^8.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"mocha": {
"spec": "build/test/**/test-*.js",

View file

@ -1,4 +1,4 @@
import * as xml2js from 'xml2js';
import { xml2js } from '@keymanapp/common-types';
import JSZip from 'jszip';
import KEYMAN_VERSION from "@keymanapp/keyman-version";

View file

@ -134,5 +134,9 @@ export class CompilerMessages {
static ERROR_FileRecordIsMissingName = SevError | 0x001F;
static Error_FileRecordIsMissingName = (o:{description:string}) => m(this.ERROR_FileRecordIsMissingName,
`File record in the package with description '${o.description}' is missing a filename.`);
static ERROR_InvalidAuthorEmail = SevError | 0x0020;
static Error_InvalidAuthorEmail = (o:{email:string}) => m(this.ERROR_InvalidAuthorEmail,
`Invalid author email: ${def(o.email)}`);
}

View file

@ -1,6 +1,7 @@
import { KmpJsonFile, CompilerCallbacks, CompilerOptions, KeymanFileTypes } from '@keymanapp/common-types';
import { CompilerMessages } from './package-compiler-messages.js';
import { keymanEngineForWindowsFiles, keymanForWindowsInstallerFiles, keymanForWindowsRedistFiles } from './redist-files.js';
import { isValidEmail } from '@keymanapp/developer-utils';
// The keyboard ID SHOULD adhere to this pattern:
const KEYBOARD_ID_PATTERN_PACKAGE = /^[a-z_][a-z0-9_]*\.(kps|kmp)$/;
@ -208,6 +209,21 @@ export class PackageValidation {
return false;
}
if(file.info?.author?.url) {
// we strip the mailto: from the .kps file for the .model_info
const match = file.info.author.url.match(/^(mailto\:)?(.+)$/);
/* c8 ignore next 3 */
if (match === null) {
this.callbacks.reportMessage(CompilerMessages.Error_InvalidAuthorEmail({email:file.info.author.url}));
return null;
}
if(!isValidEmail(match[2])) {
this.callbacks.reportMessage(CompilerMessages.Error_InvalidAuthorEmail({email:file.info.author.url}));
return null;
}
}
return true;
}
}

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>15.0.266.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Info>
<!-- blank name -->
<Name URL="">Invalid Email Address</Name>
<Copyright URL="">© 2019 National Research Council Canada</Copyright>
<Author URL="&lt;Eddie Santos&gt; mailto:Eddie.Santos@nrc-cnrc.gc.ca">Eddie Antonio Santos</Author>
<Version>1.0</Version>
</Info>
<Files>
<File>
<Name>basic.kmx</Name>
<Description>Keyboard Basic</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Basic</Name>
<ID>basic</ID>
<Version>1.0</Version>
<Languages>
<Language ID="km">Central Khmer (Khmer, Cambodia)</Language>
</Languages>
</Keyboard>
</Keyboards>
</Package>

View file

@ -0,0 +1,32 @@
<?xml version="1.0" encoding="utf-8"?>
<Package>
<System>
<KeymanDeveloperVersion>15.0.266.0</KeymanDeveloperVersion>
<FileVersion>7.0</FileVersion>
</System>
<Info>
<!-- blank name -->
<Name URL="">Invalid Email Address</Name>
<Copyright URL="">© 2019 National Research Council Canada</Copyright>
<Author URL="mailto:Eddie.Santos@nrc-cnrc.gc.ca, example@example.com">Eddie Antonio Santos</Author>
<Version>1.0</Version>
</Info>
<Files>
<File>
<Name>basic.kmx</Name>
<Description>Keyboard Basic</Description>
<CopyLocation>0</CopyLocation>
<FileType>.kmx</FileType>
</File>
</Files>
<Keyboards>
<Keyboard>
<Name>Basic</Name>
<ID>basic</ID>
<Version>1.0</Version>
<Languages>
<Language ID="km">Central Khmer (Khmer, Cambodia)</Language>
</Languages>
</Keyboard>
</Keyboards>
</Package>

View file

@ -224,4 +224,16 @@ describe('CompilerMessages', function () {
CompilerMessages.ERROR_InvalidPackageFile);
});
// ERROR_InvalidAuthorEmail
it('should generate ERROR_InvalidAuthorEmail if author email address has multiple addresses', async function() {
await testForMessage(this, ['invalid', 'error_invalid_author_email_multiple.kps'],
CompilerMessages.ERROR_InvalidAuthorEmail);
});
it('should generate ERROR_InvalidAuthorEmail if author email address is formatted incorrectly', async function() {
await testForMessage(this, ['invalid', 'error_invalid_author_email.kps'],
CompilerMessages.ERROR_InvalidAuthorEmail);
});
});

View file

@ -61,8 +61,7 @@
"c8": "^7.12.0",
"esbuild": "^0.15.8",
"mocha": "^8.4.0",
"ts-node": "^9.1.1",
"typescript": "^4.9.5"
"typescript": "^5.4.5"
},
"mocha": {
"spec": "build/test/**/test-*.js",

View file

@ -5,7 +5,7 @@
#
project('kmcmplib', 'cpp', 'c',
version: run_command(find_program('getversion.bat', 'getversion.sh')).stdout().strip(),
version: run_command(find_program('getversion.bat', 'getversion.sh'), check: true).stdout().strip(),
license: 'MIT',
default_options : ['buildtype=release',
'cpp_std=c++14',

View file

@ -1208,7 +1208,11 @@ int GetCompileTargetsFromTargetsStore(const KMX_WCHAR* store) {
}
KMX_BOOL IsValidKeyboardVersion(KMX_WCHAR *dpString) { // I4140
/* version format \d+(\.\d+)* e.g. 9.0.3, 1.0, 1.2.3.4, 6.2.1.4.6.4, blank is not allowed */
/**
version format: /^\d+(\.\d+)*$/
e.g. 9.0.3, 1.0, 1.2.3.4, 6.2.1.4.6.4, 11.22.3 are all ok;
empty string is not permitted; whitespace is not permitted
*/
do {
if (!iswdigit(*dpString)) {

Some files were not shown because too many files have changed in this diff Show more