mirror of
https://github.com/keymanapp/keyman.git
synced 2026-10-06 14:07:32 +00:00
Merge branch 'master' into feat/core/9467-devolve-regex-to-js2-epic-ldml
This commit is contained in:
commit
ff3c9736f4
159 changed files with 8945 additions and 2257 deletions
36
.github/workflows/api-verification.yml
vendored
36
.github/workflows/api-verification.yml
vendored
|
|
@ -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: |
|
||||
|
|
|
|||
61
HISTORY.md
61
HISTORY.md
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
18.0.49
|
||||
18.0.56
|
||||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -145,7 +145,7 @@ function setOskHeight(h) {
|
|||
|
||||
function setOskWidth(w) {
|
||||
if(w > 0) {
|
||||
oskWidth = w;
|
||||
oskWidth = w / window.devicePixelRatio;
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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()) {
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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": "*",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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": "*",
|
||||
|
|
|
|||
|
|
@ -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));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,6 +5,6 @@
|
|||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"@keymanapp/resources-gosh": "*",
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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`;
|
||||
|
|
@ -1,10 +0,0 @@
|
|||
{
|
||||
"extensions": ["js", "ts"],
|
||||
"spec" : [
|
||||
"src/test/auto/headless/**/*.*"
|
||||
],
|
||||
"node-option": [
|
||||
"loader=ts-node/esm"
|
||||
],
|
||||
"require": ".mocharc.env.js"
|
||||
}
|
||||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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() {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
47
common/web/gesture-recognizer/src/test/resources/@types/promiseStatus.d.ts
vendored
Normal file
47
common/web/gesture-recognizer/src/test/resources/@types/promiseStatus.d.ts
vendored
Normal 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
|
||||
}
|
||||
|
|
@ -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");
|
||||
|
|
|
|||
30
common/web/gesture-recognizer/src/test/tsconfig.json
Normal file
30
common/web/gesture-recognizer/src/test/tsconfig.json
Normal 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": "../../"}
|
||||
]
|
||||
}
|
||||
|
|
@ -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 ( ) {
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -13,6 +13,6 @@
|
|||
"license": "MIT",
|
||||
"type": "module",
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -7,7 +7,7 @@
|
|||
},
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"files": [
|
||||
"message.d.ts"
|
||||
|
|
|
|||
|
|
@ -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": "*",
|
||||
|
|
|
|||
|
|
@ -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':
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
||||
|
|
|
|||
|
|
@ -25,6 +25,6 @@
|
|||
"@keymanapp/web-utils": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.4.5"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,7 +17,7 @@
|
|||
},
|
||||
"homepage": "https://github.com/keymanapp/keyman#readme",
|
||||
"devDependencies": {
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"@keymanapp/keyman-version": "*",
|
||||
|
|
|
|||
|
|
@ -18,7 +18,7 @@
|
|||
},
|
||||
"dependencies": {
|
||||
"tslib": "^2.5.2",
|
||||
"typescript": "^4.9.5"
|
||||
"typescript": "^5.4.5"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@keymanapp/resources-gosh": "*",
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
19
common/web/types/src/deps/xml2js/LICENSE
Normal file
19
common/web/types/src/deps/xml2js/LICENSE
Normal 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.
|
||||
507
common/web/types/src/deps/xml2js/README.md
Normal file
507
common/web/types/src/deps/xml2js/README.md
Normal 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
|
||||
==========================
|
||||
|
||||
[](https://travis-ci.org/Leonidas-from-XIV/node-xml2js)
|
||||
[](https://coveralls.io/r/Leonidas-from-XIV/node-xml2js?branch=master)
|
||||
[](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.
|
||||
8
common/web/types/src/deps/xml2js/bom.js
Normal file
8
common/web/types/src/deps/xml2js/bom.js
Normal file
|
|
@ -0,0 +1,8 @@
|
|||
"use strict";
|
||||
export function stripBOM(str) {
|
||||
if (str[0] === '\uFEFF') {
|
||||
return str.substring(1);
|
||||
} else {
|
||||
return str;
|
||||
}
|
||||
};
|
||||
118
common/web/types/src/deps/xml2js/builder.js
Normal file
118
common/web/types/src/deps/xml2js/builder.js
Normal 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);
|
||||
};
|
||||
}
|
||||
69
common/web/types/src/deps/xml2js/defaults.js
Normal file
69
common/web/types/src/deps/xml2js/defaults.js
Normal 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
|
||||
}
|
||||
};
|
||||
377
common/web/types/src/deps/xml2js/parser.js
Normal file
377
common/web/types/src/deps/xml2js/parser.js
Normal 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);
|
||||
};
|
||||
|
||||
31
common/web/types/src/deps/xml2js/processors.js
Normal file
31
common/web/types/src/deps/xml2js/processors.js
Normal 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;
|
||||
};
|
||||
|
||||
27
common/web/types/src/deps/xml2js/xml2js.js
Normal file
27
common/web/types/src/deps/xml2js/xml2js.js
Normal 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;
|
||||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -9,6 +9,7 @@
|
|||
"preserveConstEnums": true,
|
||||
},
|
||||
"include": [
|
||||
"src/deps/xml2js/*.js",
|
||||
"src/**/*.ts",
|
||||
"src/schemas/*.mjs", // Import the validators
|
||||
],
|
||||
|
|
|
|||
|
|
@ -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": {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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());
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.')
|
||||
|
|
|
|||
4
core/tests/unit/kmx/cat.bat
Normal file
4
core/tests/unit/kmx/cat.bat
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
@echo off
|
||||
set infile=%1
|
||||
set infileb=%infile:/=\%
|
||||
type %infileb%
|
||||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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()
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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]
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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')
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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';
|
||||
|
|
@ -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"
|
||||
},
|
||||
|
|
|
|||
18
developer/src/common/web/utils/src/is-valid-email.ts
Normal file
18
developer/src/common/web/utils/src/is-valid-email.ts
Normal 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);
|
||||
}
|
||||
35
developer/src/common/web/utils/test/test-is-valid-email.ts
Normal file
35
developer/src/common/web/utils/test/test-is-valid-email.ts
Normal 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`));
|
||||
});
|
||||
});
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
|
|
|
|||
21
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/LICENSE.md
vendored
Normal file
21
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/LICENSE.md
vendored
Normal 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.
|
||||
1
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/.gitattributes
vendored
Normal file
1
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/.gitattributes
vendored
Normal file
|
|
@ -0,0 +1 @@
|
|||
khmer_angkor.js -text
|
||||
5462
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.js
vendored
Normal file
5462
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.js
vendored
Normal file
File diff suppressed because it is too large
Load diff
BIN
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmp
vendored
Normal file
BIN
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmp
vendored
Normal file
Binary file not shown.
BIN
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmx
vendored
Normal file
BIN
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/build/khmer_angkor.kmx
vendored
Normal file
Binary file not shown.
187
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/khmer_angkor.kpj
vendored
Normal file
187
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/khmer_angkor.kpj
vendored
Normal 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>
|
||||
49
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/source/khmer_angkor.kps
vendored
Normal file
49
developer/src/kmc-keyboard-info/test/fixtures/multiple-email-addresses/source/khmer_angkor.kps
vendored
Normal 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>
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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];
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -33,6 +33,7 @@ builder_parse "$@"
|
|||
#-------------------------------------------------------------------------------------------------------------------
|
||||
|
||||
function do_build() {
|
||||
tsc -b ./tools/tsconfig.json
|
||||
npm run build
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
|
|
|||
13
developer/src/kmc-model/tools/tsconfig.json
Normal file
13
developer/src/kmc-model/tools/tsconfig.json
Normal file
|
|
@ -0,0 +1,13 @@
|
|||
{
|
||||
"extends": "../tsconfig.json",
|
||||
|
||||
"compilerOptions": {
|
||||
"outDir": "../build/tools/",
|
||||
"rootDir": ".",
|
||||
"baseUrl": "../.",
|
||||
"allowSyntheticDefaultImports": true,
|
||||
},
|
||||
"include": [
|
||||
"./*.ts"
|
||||
]
|
||||
}
|
||||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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";
|
||||
|
||||
|
|
|
|||
|
|
@ -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)}`);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
32
developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email.kps
vendored
Normal file
32
developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email.kps
vendored
Normal 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="<Eddie Santos> 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>
|
||||
32
developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email_multiple.kps
vendored
Normal file
32
developer/src/kmc-package/test/fixtures/invalid/error_invalid_author_email_multiple.kps
vendored
Normal 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>
|
||||
|
|
@ -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);
|
||||
});
|
||||
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -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
Loading…
Add table
Reference in a new issue