Merge pull request #10274 from keymanapp/feat/web/webview-es6-target-artifact

feat(web): es6 artifact for app/webview 📜
This commit is contained in:
Joshua Horton 2024-01-08 15:54:12 +07:00 committed by GitHub
commit 87acd6a2cf
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
11 changed files with 213 additions and 152 deletions

2
android/.gitignore vendored
View file

@ -37,6 +37,8 @@ KMEA/**/assets/keymanandroid.js
KMEA/**/assets/keyman.js.map
KMEA/**/assets/keymanweb-webview.js
KMEA/**/assets/keymanweb-webview.js.map
KMEA/**/assets/keymanweb-webview.es5.js
KMEA/**/assets/keymanweb-webview.es5.js.map
KMEA/**/assets/map-polyfill.js
KMEA/**/assets/sentry.min.js
KMEA/**/assets/keyman-sentry.js

View file

@ -0,0 +1,30 @@
<!DOCTYPE html>
<html>
<!--
Copyright (C) 2017-2019 SIL International. All rights reserved.
-->
<head>
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>Keyman</title>
<!-- Provides polyfills needed by un-updated Chromium versions found
on older, but still-supported, Android APIs.
If and when our minimum supported Android version shipped with
Chromium version 54 or greater, we should be able to drop this
polyfill.
-->
<script src="es6-shim.min.js"></script>
<script src="other-polyfills.js"></script>
<script src="map-polyfill.js"></script>
<script src="keymanweb-webview.es5.js"></script>
<script src="sentry.min.js"></script>
<script src="keyman-sentry.js"></script>
<script src="android-host.js"></script>
<style type="text/css">
body {background-color:#000000;}
</style>
</head>
<body class="kmw-embedded keyman-app">
</body>
</html>

View file

@ -7,16 +7,6 @@
<meta http-equiv="content-type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, user-scalable=no">
<title>Keyman</title>
<!-- Provides polyfills needed by un-updated Chromium versions found
on older, but still-supported, Android APIs.
If and when our minimum supported Android version shipped with
Chromium version 54 or greater, we should be able to drop this
polyfill.
-->
<script src="es6-shim.min.js"></script>
<script src="other-polyfills.js"></script>
<script src="map-polyfill.js"></script>
<script src="keymanweb-webview.js"></script>
<script src="sentry.min.js"></script>
<script src="keyman-sentry.js"></script>

View file

@ -288,8 +288,11 @@ public final class KMManager {
// Keyman files
protected static final String KMFilename_KeyboardHtml = "keyboard.html";
protected static final String KMFilename_KeyboardHtml_Legacy = "keyboard.es5.html";
protected static final String KMFilename_JSEngine = "keymanweb-webview.js";
protected static final String KMFilename_JSEngine_Sourcemap = "keymanweb-webview.js.map";
protected static final String KMFilename_JSLegacyEngine = "keymanweb-webview.es5.js";
protected static final String KMFilename_JSLegacyEngine_Sourcemap = "keymanweb-webview.es5.js.map";
protected static final String KMFilename_JSSentry = "sentry.min.js";
protected static final String KMFilename_JSSentryInit = "keyman-sentry.js";
protected static final String KMFilename_AndroidHost = "android-host.js";
@ -849,25 +852,47 @@ public final class KMManager {
private static void copyAssets(Context context) {
AssetManager assetManager = context.getAssets();
// Will build a temp WebView in order to check Chrome version internally.
boolean legacyMode = WebViewUtils.getEngineWebViewVersionStatus(context, null, null) != WebViewUtils.EngineWebViewVersionStatus.FULL;
try {
// Copy KMW files
copyAsset(context, KMFilename_KeyboardHtml, "", true);
copyAsset(context, KMFilename_JSEngine, "", true);
if(legacyMode) {
// Replaces the standard ES6-friendly version of the host page with a legacy one that
// includes polyfill requests and that links the legacy, ES5-compatible version of KMW.
copyAssetWithRename(context, KMFilename_KeyboardHtml_Legacy, KMFilename_KeyboardHtml, "", true);
copyAsset(context, KMFilename_JSLegacyEngine, "", true);
if (KMManager.isDebugMode()) {
copyAsset(context, KMFilename_JSLegacyEngine_Sourcemap, "", true);
}
} else {
copyAsset(context, KMFilename_KeyboardHtml, "", true);
// For versions of Chrome with full ES6 support, we use the ES6 artifact.
copyAsset(context, KMFilename_JSEngine, "", true);
if (KMManager.isDebugMode()) {
copyAsset(context, KMFilename_JSEngine_Sourcemap, "", true);
}
}
// Is still built targeting ES5.
copyAsset(context, KMFilename_JSSentry, "", true);
copyAsset(context, KMFilename_JSSentryInit, "", true);
copyAsset(context, KMFilename_AndroidHost, "", true);
if(KMManager.isDebugMode()) {
copyAsset(context, KMFilename_JSEngine_Sourcemap, "", true);
}
copyAsset(context, KMFilename_KmwCss, "", true);
copyAsset(context, KMFilename_KmwGlobeHintCss, "", true);
copyAsset(context, KMFilename_Osk_Ttf_Font, "", true);
// Copy default keyboard font
copyAsset(context, KMDefault_KeyboardFont, "", true);
copyAsset(context, KMFilename_JSPolyfill, "", true);
copyAsset(context, KMFilename_JSPolyfill2, "", true);
copyAsset(context, KMFilename_JSPolyfill3, "", true);
if(legacyMode) {
copyAsset(context, KMFilename_JSPolyfill, "", true);
copyAsset(context, KMFilename_JSPolyfill2, "", true);
copyAsset(context, KMFilename_JSPolyfill3, "", true);
}
// Keyboard packages directory
File packagesDir = new File(getPackagesDir());
@ -963,6 +988,10 @@ public final class KMManager {
}
private static int copyAsset(Context context, String filename, String directory, boolean overwrite) {
return copyAssetWithRename(context, filename, filename, directory, overwrite);
}
private static int copyAssetWithRename(Context context, String srcName, String destName, String directory, boolean overwrite) {
int result;
AssetManager assetManager = context.getAssets();
try {
@ -979,9 +1008,9 @@ public final class KMManager {
dirPath = getResourceRoot();
}
File file = new File(dirPath, filename);
File file = new File(dirPath, destName);
if (!file.exists() || overwrite) {
InputStream inputStream = assetManager.open(directory + filename);
InputStream inputStream = assetManager.open(directory + srcName);
FileOutputStream outputStream = new FileOutputStream(file);
FileUtils.copy(inputStream, outputStream);

View file

@ -84,6 +84,8 @@ if builder_start_action build:engine; then
echo "Copying Keyman Web artifacts"
cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js" "$ENGINE_ASSETS/keymanweb-webview.js"
cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.js.map" "$ENGINE_ASSETS/keymanweb-webview.js.map"
cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.es5.js" "$ENGINE_ASSETS/keymanweb-webview.es5.js"
cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/keymanweb-webview.es5.js.map" "$ENGINE_ASSETS/keymanweb-webview.es5.js.map"
cp "$KEYMAN_WEB_ROOT/build/app/webview/$CONFIG/map-polyfill.js" "$ENGINE_ASSETS/map-polyfill.js"
cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/ajax-loader.gif" "$ENGINE_ASSETS/ajax-loader.gif"
cp "$KEYMAN_WEB_ROOT/build/app/resources/osk/kmwosk.css" "$ENGINE_ASSETS/kmwosk.css"

View file

@ -1,22 +1,8 @@
import {
export {
SENTINEL_CODE_UNIT, applyTransform, buildMergedTransform, isHighSurrogate, isLowSurrogate, isSentinel,
transformToSuggestion, defaultApplyCasing
} from "./common.js";
import PriorityQueue, { Comparator } from "./priority-queue.js";
import QuoteBehavior from "./quote-behavior.js";
import { Tokenization, tokenize, getLastPreCaretToken, wordbreak } from "./tokenization.js";
import TrieModel, { TrieModelOptions } from "./trie-model.js";
import { extendString } from "@keymanapp/web-utils";
// This package requires our string-extension functions.
extendString();
export {
SENTINEL_CODE_UNIT, applyTransform, buildMergedTransform, isHighSurrogate, isLowSurrogate, isSentinel,
transformToSuggestion, defaultApplyCasing, // "common.ts"
PriorityQueue, Comparator, // "priority-queue.ts"
QuoteBehavior, // "quote-behavior.ts",
Tokenization, tokenize, getLastPreCaretToken, wordbreak, // "tokenization.ts"
TrieModel, TrieModelOptions // "trie-model.ts"
};
export { default as PriorityQueue, Comparator } from "./priority-queue.js";
export { default as QuoteBehavior } from "./quote-behavior.js";
export { Tokenization, tokenize, getLastPreCaretToken, wordbreak } from "./tokenization.js";
export { default as TrieModel, TrieModelOptions } from "./trie-model.js";

View file

@ -33,6 +33,8 @@ import { applyTransform, isHighSurrogate, isSentinel, SENTINEL_CODE_UNIT, transf
import { getLastPreCaretToken } from "./tokenization.js";
import PriorityQueue from "./priority-queue.js";
extendString();
/**
* @file trie-model.ts
*
@ -81,6 +83,114 @@ type TextWithProbability = {
p: number; // real-number weight, from 0 to 1
}
class Traversal implements LexiconTraversal {
/**
* The lexical prefix corresponding to the current traversal state.
*/
prefix: String;
/**
* The current traversal node. Serves as the 'root' of its own sub-Trie,
* and we cannot navigate back to its parent.
*/
root: Node;
constructor(root: Node, prefix: string) {
this.root = root;
this.prefix = prefix;
}
*children(): Generator<{char: string, traversal: () => LexiconTraversal}> {
let root = this.root;
if(root.type == 'internal') {
for(let entry of root.values) {
let entryNode = root.children[entry];
// UTF-16 astral plane check.
if(isHighSurrogate(entry)) {
// First code unit of a UTF-16 code point.
// For now, we'll just assume the second always completes such a char.
//
// Note: Things get nasty here if this is only sometimes true; in the future,
// we should compile-time enforce that this assumption is always true if possible.
if(entryNode.type == 'internal') {
let internalNode = entryNode;
for(let lowSurrogate of internalNode.values) {
let prefix = this.prefix + entry + lowSurrogate;
yield {
char: entry + lowSurrogate,
traversal: function() { return new Traversal(internalNode.children[lowSurrogate], prefix) }
}
}
} else {
// Determine how much of the 'leaf' entry has no Trie nodes, emulate them.
let fullText = entryNode.entries[0].key;
entry = entry + fullText[this.prefix.length + 1]; // The other half of the non-BMP char.
let prefix = this.prefix + entry;
yield {
char: entry,
traversal: function () {return new Traversal(entryNode, prefix)}
}
}
} else if(isSentinel(entry)) {
continue;
} else if(!entry) {
// Prevent any accidental 'null' or 'undefined' entries from having an effect.
continue;
} else {
let prefix = this.prefix + entry;
yield {
char: entry,
traversal: function() { return new Traversal(entryNode, prefix)}
}
}
}
return;
} else { // type == 'leaf'
let prefix = this.prefix;
let children = root.entries.filter(function(entry) {
return entry.key != prefix && prefix.length < entry.key.length;
})
for(let {key} of children) {
let nodeKey = key[prefix.length];
if(isHighSurrogate(nodeKey)) {
// Merge the other half of an SMP char in!
nodeKey = nodeKey + key[prefix.length+1];
}
yield {
char: nodeKey,
traversal: function() { return new Traversal(root, prefix + nodeKey)}
}
};
return;
}
}
get entries(): string[] {
if(this.root.type == 'leaf') {
let prefix = this.prefix;
let matches = this.root.entries.filter(function(entry) {
return entry.key == prefix;
});
return matches.map(function(value) { return value.content });
} else {
let matchingLeaf = this.root.children[SENTINEL_CODE_UNIT];
if(matchingLeaf && matchingLeaf.type == 'leaf') {
return matchingLeaf.entries.map(function(value) { return value.content });
} else {
return [];
}
}
}
}
/**
* @class TrieModel
*
@ -176,115 +286,7 @@ export default class TrieModel implements LexicalModel {
}
public traverseFromRoot(): LexiconTraversal {
return new TrieModel.Traversal(this._trie['root'], '');
}
private static Traversal = class implements LexiconTraversal {
/**
* The lexical prefix corresponding to the current traversal state.
*/
prefix: String;
/**
* The current traversal node. Serves as the 'root' of its own sub-Trie,
* and we cannot navigate back to its parent.
*/
root: Node;
constructor(root: Node, prefix: string) {
this.root = root;
this.prefix = prefix;
}
*children(): Generator<{char: string, traversal: () => LexiconTraversal}> {
let root = this.root;
if(root.type == 'internal') {
for(let entry of root.values) {
let entryNode = root.children[entry];
// UTF-16 astral plane check.
if(isHighSurrogate(entry)) {
// First code unit of a UTF-16 code point.
// For now, we'll just assume the second always completes such a char.
//
// Note: Things get nasty here if this is only sometimes true; in the future,
// we should compile-time enforce that this assumption is always true if possible.
if(entryNode.type == 'internal') {
let internalNode = entryNode;
for(let lowSurrogate of internalNode.values) {
let prefix = this.prefix + entry + lowSurrogate;
yield {
char: entry + lowSurrogate,
traversal: function() { return new TrieModel.Traversal(internalNode.children[lowSurrogate], prefix) }
}
}
} else {
// Determine how much of the 'leaf' entry has no Trie nodes, emulate them.
let fullText = entryNode.entries[0].key;
entry = entry + fullText[this.prefix.length + 1]; // The other half of the non-BMP char.
let prefix = this.prefix + entry;
yield {
char: entry,
traversal: function () {return new TrieModel.Traversal(entryNode, prefix)}
}
}
} else if(isSentinel(entry)) {
continue;
} else if(!entry) {
// Prevent any accidental 'null' or 'undefined' entries from having an effect.
continue;
} else {
let prefix = this.prefix + entry;
yield {
char: entry,
traversal: function() { return new TrieModel.Traversal(entryNode, prefix)}
}
}
}
return;
} else { // type == 'leaf'
let prefix = this.prefix;
let children = root.entries.filter(function(entry) {
return entry.key != prefix && prefix.length < entry.key.length;
})
for(let {key} of children) {
let nodeKey = key[prefix.length];
if(isHighSurrogate(nodeKey)) {
// Merge the other half of an SMP char in!
nodeKey = nodeKey + key[prefix.length+1];
}
yield {
char: nodeKey,
traversal: function() { return new TrieModel.Traversal(root, prefix + nodeKey)}
}
};
return;
}
}
get entries(): string[] {
if(this.root.type == 'leaf') {
let prefix = this.prefix;
let matches = this.root.entries.filter(function(entry) {
return entry.key == prefix;
});
return matches.map(function(value) { return value.content });
} else {
let matchingLeaf = this.root.children[SENTINEL_CODE_UNIT];
if(matchingLeaf && matchingLeaf.type == 'leaf') {
return matchingLeaf.entries.map(function(value) { return value.content });
} else {
return [];
}
}
}
return new Traversal(this._trie['root'], '');
}
};

View file

@ -105,6 +105,6 @@ function do_test() {
}
builder_run_action configure verify_npm_setup
builder_run_action clean rm -rf build/
builder_run_action clean rm -rf build/ intermediate/
builder_run_action build do_build
builder_run_action test do_test

View file

@ -36,5 +36,8 @@
"type": "module",
"paths": {
"@keymanapp/keyman-version": "*"
}
},
"sideEffects": [
"./src/kmwstring.ts", "./build/obj/kmwstring.js"
]
}

View file

@ -44,16 +44,29 @@ compile_and_copy() {
compile $SUBPROJECT_NAME
BUILD_ROOT="${KEYMAN_ROOT}/web/build/app/webview"
SRC_ROOT="${KEYMAN_ROOT}/web/src/app/webview/src"
$BUNDLE_CMD "${BUILD_ROOT}/obj/debug-main.js" \
--out "${BUILD_ROOT}/debug/keymanweb-webview.js" \
--out "${BUILD_ROOT}/debug/keymanweb-webview.es5.js" \
--sourceRoot "@keymanapp/keyman/web/build/app/webview/debug"
$BUNDLE_CMD "${BUILD_ROOT}/obj/release-main.js" \
--out "${BUILD_ROOT}/release/keymanweb-webview.es5.js" \
--profile "${BUILD_ROOT}/filesize-profile.es5.log" \
--sourceRoot "@keymanapp/keyman/web/build/app/webview/release" \
--minify
$BUNDLE_CMD "${SRC_ROOT}/debug-main.js" \
--out "${BUILD_ROOT}/debug/keymanweb-webview.js" \
--sourceRoot "@keymanapp/keyman/web/build/app/webview/debug" \
--target "es6"
$BUNDLE_CMD "${SRC_ROOT}/release-main.js" \
--out "${BUILD_ROOT}/release/keymanweb-webview.js" \
--profile "${BUILD_ROOT}/filesize-profile.log" \
--sourceRoot "@keymanapp/keyman/web/build/app/webview/release" \
--minify
--minify \
--target "es6"
mkdir -p "$KEYMAN_ROOT/web/build/app/resources/osk"
cp -R "$KEYMAN_ROOT/web/src/resources/osk/." "$KEYMAN_ROOT/web/build/app/resources/osk/"

View file

@ -34,6 +34,10 @@ export class StaticActivator extends Activator {
return true;
}
set enabled(value: boolean) {
// does nothing; it's static.
}
get activate(): boolean {
return true;
}