mirror of
https://github.com/keymanapp/keyman.git
synced 2026-09-28 10:37:41 +00:00
chore(web): Merge branch 'master' into fix/web/legacy-kbd-modifier-tracking
This commit is contained in:
commit
010f0b9eb5
51 changed files with 1860 additions and 670 deletions
25
HISTORY.md
25
HISTORY.md
|
|
@ -1,5 +1,30 @@
|
|||
# Keyman Version History
|
||||
|
||||
## 17.0.248 alpha 2024-01-19
|
||||
|
||||
* chore(android): Update targetSdkVersion to 34 (#10393)
|
||||
* (#10352)
|
||||
* fix(web): keyboard-documentation rendering mode (#10417)
|
||||
* fix(ios): multiple keyboard slide-in animations on app start (#10362)
|
||||
* fix(web): U_ key id -> text for all subkeys; is now preprocessed (#10434)
|
||||
* chore(android/app): Update whatsnew for 17.0 (#10395)
|
||||
* fix(web): bulk renderer interface for recent-version targeting keyboards (#10427)
|
||||
* feat(core): cross-segment markers (#10394)
|
||||
|
||||
## 17.0.247 alpha 2024-01-18
|
||||
|
||||
* fix(ios): banner image management (#10337)
|
||||
* feat(core): unescape u (#10356)
|
||||
* chore(developer,core): change sample and test files to use \u{…} (#10391)
|
||||
|
||||
## 17.0.246 alpha 2024-01-17
|
||||
|
||||
* fix(web): Add null check for changing the keyboard during typing (#10346)
|
||||
* chore(common): Update crowdin strings for Khmer (#10411)
|
||||
* docs(common): Update website README.md (#10399)
|
||||
* docs(linux): Add documentation for Core API verification (#10409)
|
||||
* fix(web): right-flick gesture-preview positioning (#10406)
|
||||
|
||||
## 17.0.245 alpha 2024-01-16
|
||||
|
||||
* chore(core): Ignore C++ symbols (#10386)
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
17.0.246
|
||||
17.0.249
|
||||
|
|
@ -10,7 +10,7 @@ ext.rootPath = '../../'
|
|||
apply from: "$rootPath/version.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdk 34
|
||||
namespace="com.tavultesoft.kmapro"
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
|
|
@ -21,7 +21,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.tavultesoft.kmapro"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
|
||||
//println "===DUMPING PROPERTIES==="
|
||||
//dumpProperties(project) // Use this to dump all external properties for debugging TeamCity integration
|
||||
|
|
|
|||
|
|
@ -7,12 +7,12 @@ ext.rootPath = '../../'
|
|||
apply from: "$rootPath/version.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdk 34
|
||||
namespace "com.keyman.engine"
|
||||
|
||||
defaultConfig {
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
|
||||
// VERSION_CODE and VERSION_NAME from version.gradle but Gradle removes them for libraries
|
||||
buildConfigField "String", "KEYMAN_ENGINE_VERSION_NAME", "\""+VERSION_NAME+"\""
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdk 34
|
||||
namespace="com.keyman.kmsample1"
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
|
|
@ -14,7 +14,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.keyman.kmsample1"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@ plugins {
|
|||
}
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdk 34
|
||||
namespace="com.keyman.kmsample2"
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
|
|
@ -14,7 +14,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.keyman.kmsample2"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
versionCode 1
|
||||
versionName "1.0"
|
||||
}
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ ext.rootPath = '../../../'
|
|||
apply from: "$rootPath/version.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdk 34
|
||||
namespace="com.keyman.android.tests.keyboardHarness"
|
||||
|
||||
// Don't compress kmp files so they can be copied via AssetManager
|
||||
|
|
@ -21,7 +21,7 @@ android {
|
|||
defaultConfig {
|
||||
applicationId "com.keyman.android.tests.keyboardHarness"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
|
||||
// VERSION_CODE and VERSION_NAME from version.gradle
|
||||
versionCode VERSION_CODE as Integer
|
||||
|
|
|
|||
|
|
@ -4,13 +4,12 @@ ext.rootPath = '../../../'
|
|||
apply from: "$rootPath/version.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
namespace="com.keyman.android.tests.keycode"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.keyman.android.tests.keycode"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
|
||||
// VERSION_CODE and VERSION_NAME from version.gradle
|
||||
versionCode VERSION_CODE as Integer
|
||||
|
|
|
|||
|
|
@ -2,3 +2,13 @@
|
|||
title: What's New
|
||||
---
|
||||
Here are some of the new features we have added to Keyman 17.0 for Android:
|
||||
|
||||
* New gesture support (#5029)
|
||||
* When suggestions aren't enabled, display a themed banner (#9696)
|
||||
* Smoother keyboard initialization (#10022)
|
||||
|
||||
Additional changes:
|
||||
|
||||
* Remove built-in browser (#8428)
|
||||
* Use web-based popup key longpresses (#9591)
|
||||
* Performance improvements
|
||||
|
|
|
|||
|
|
@ -33,6 +33,11 @@ export class GestureRecognizer<HoveredItemType, StateToken = any> extends Touchp
|
|||
}
|
||||
|
||||
public destroy() {
|
||||
// When shutting down the gesture engine, we should go ahead and clear out all related
|
||||
// gesture-source tracking.
|
||||
this.activeGestures.forEach((sequence) => sequence.cancel());
|
||||
this.activeSources.forEach((source) => source.terminate(true));
|
||||
|
||||
this.mouseEngine.unregisterEventHandlers();
|
||||
this.touchEngine.unregisterEventHandlers();
|
||||
|
||||
|
|
|
|||
|
|
@ -331,6 +331,10 @@ export class ActiveKeyBase {
|
|||
}
|
||||
}
|
||||
|
||||
if(!key.text && typeof key.id == 'string') {
|
||||
key.text = ActiveKey.unicodeIDToText(key.id);
|
||||
}
|
||||
|
||||
// Ensure subkeys are also properly extended.
|
||||
if(key.sk) {
|
||||
analysisFlagObj.hasLongpresses = true;
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
*/
|
||||
|
||||
import { constants } from "@keymanapp/ldml-keyboard-constants";
|
||||
import { MATCH_QUAD_ESCAPE, isOneChar, unescapeOneQuadString, unescapeString } from "../util/util.js";
|
||||
import { MATCH_QUAD_ESCAPE, isOneChar, unescapeOneQuadString, unescapeString, hexQuad } from "../util/util.js";
|
||||
|
||||
|
||||
/**
|
||||
|
|
@ -51,10 +51,12 @@ export class MarkerParser {
|
|||
* Marker sentinel as a string - U+FFFF
|
||||
*/
|
||||
public static readonly SENTINEL = String.fromCodePoint(constants.uc_sentinel);
|
||||
static readonly SENTINEL_MATCH = '\\u' + hexQuad(constants.uc_sentinel);
|
||||
/**
|
||||
* Marker code as a string - U+0008
|
||||
*/
|
||||
public static readonly MARKER_CODE = String.fromCodePoint(constants.marker_code);
|
||||
static readonly MARKER_CODE_MATCH = '\\u' + hexQuad(constants.marker_code);
|
||||
|
||||
/** Minimum ID (trailing code unit) */
|
||||
public static readonly MIN_MARKER_INDEX = constants.marker_min_index;
|
||||
|
|
@ -65,18 +67,10 @@ export class MarkerParser {
|
|||
/** Max count of markers */
|
||||
public static readonly MAX_MARKER_COUNT = constants.marker_max_count;
|
||||
|
||||
/** 0000 … FFFF */
|
||||
private static hexQuad(n: number): string {
|
||||
if (n < 0x000 || n > 0xFFFF) {
|
||||
throw RangeError(`${n} not in [0x0000,0xFFFF]`);
|
||||
}
|
||||
return n.toString(16).padStart(4, '0');
|
||||
}
|
||||
|
||||
private static anyMarkerMatch() : string {
|
||||
const start = MarkerParser.hexQuad(this.MIN_MARKER_INDEX);
|
||||
const end = MarkerParser.hexQuad(this.MAX_MARKER_INDEX);
|
||||
return `${this.SENTINEL}${this.MARKER_CODE}[\\u${start}-\\u${end}]`; // TODO-LDML: #9121 wrong escape format
|
||||
const start = hexQuad(this.MIN_MARKER_INDEX);
|
||||
const end = hexQuad(this.MAX_MARKER_INDEX);
|
||||
return `${this.SENTINEL_MATCH}${this.MARKER_CODE_MATCH}[\\u${start}-\\u${end}]`; // TODO-LDML: #9121 wrong escape format
|
||||
}
|
||||
|
||||
/** Expression that matches any marker */
|
||||
|
|
@ -103,7 +97,7 @@ export class MarkerParser {
|
|||
if (!forMatch) {
|
||||
return String.fromCharCode(n);
|
||||
} else {
|
||||
return `\\u${MarkerParser.hexQuad(n)}`; // TODO-LDML: #9121 wrong escape format
|
||||
return `\\u${hexQuad(n)}`; // TODO-LDML: #9121 wrong escape format
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -112,7 +106,11 @@ export class MarkerParser {
|
|||
if (n < MarkerParser.MIN_MARKER_INDEX || n > MarkerParser.ANY_MARKER_INDEX) {
|
||||
throw RangeError(`Internal Error: marker index out of range ${n}`);
|
||||
}
|
||||
return this.SENTINEL + this.MARKER_CODE + this.markerCodeToString(n, forMatch);
|
||||
if (forMatch) {
|
||||
return this.SENTINEL_MATCH + this.MARKER_CODE_MATCH + this.markerCodeToString(n, forMatch);
|
||||
} else {
|
||||
return this.SENTINEL + this.MARKER_CODE + this.markerCodeToString(n, forMatch);
|
||||
}
|
||||
}
|
||||
|
||||
/** @returns all marker strings as sentinel values */
|
||||
|
|
|
|||
|
|
@ -22,8 +22,11 @@ export function boxXmlArray(o: any, x: string): void {
|
|||
export const MATCH_HEX_ESCAPE = /\\u{([0-9a-fA-F ]{1,})}/g;
|
||||
// const MATCH_HEX_ESCAPE = /\\u{((?:(?:[0-9a-fA-F]{1,5})|(?:10[0-9a-fA-F]{4})(?: (?!}))?)+)}/g;
|
||||
|
||||
/** regex for single quad escape such as \u0127 or \U00000000 */
|
||||
export const CONTAINS_QUAD_ESCAPE = /(?:\\u([0-9a-fA-F]{4})|\\U([0-9a-fA-F]{8}))/;
|
||||
|
||||
/** regex for single quad escape such as \u0127 */
|
||||
export const MATCH_QUAD_ESCAPE = /\\u([0-9a-fA-F]{4})/g;
|
||||
export const MATCH_QUAD_ESCAPE = new RegExp(CONTAINS_QUAD_ESCAPE, 'g');
|
||||
|
||||
export class UnescapeError extends Error {
|
||||
}
|
||||
|
|
@ -39,8 +42,10 @@ function unescapeOne(hex: string): string {
|
|||
}
|
||||
|
||||
/**
|
||||
* Unescape one single quad string such as \u0127.
|
||||
* Unescape one single quad string such as \u0127 / \U00000000
|
||||
* Throws exception if the string doesn't match MATCH_QUAD_ESCAPE
|
||||
* Note this does not attempt to handle or reject surrogates.
|
||||
* So, `\\uD838\\uDD09` will work but other combinations may not.
|
||||
* @param s input string
|
||||
* @returns output
|
||||
*/
|
||||
|
|
@ -48,13 +53,20 @@ export function unescapeOneQuadString(s: string): string {
|
|||
if (!s || !s.match(MATCH_QUAD_ESCAPE)) {
|
||||
throw new UnescapeError(`Not a quad escape: ${s}`);
|
||||
}
|
||||
function processMatch(str: string, matched: string): string {
|
||||
return unescapeOne(matched);
|
||||
function processMatch(str: string, m16: string, m32: string): string {
|
||||
return unescapeOne(m16 || m32); // either \u or \U
|
||||
}
|
||||
s = s.replace(MATCH_QUAD_ESCAPE, processMatch);
|
||||
return s;
|
||||
}
|
||||
|
||||
/** unscape multiple occurences of \u0127 style strings */
|
||||
export function unescapeQuadString(s: string): string {
|
||||
s = s.replaceAll(MATCH_QUAD_ESCAPE, (quad) => unescapeOneQuadString(quad));
|
||||
return s;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Unescapes a string according to UTS#18§1.1, see <https://www.unicode.org/reports/tr18/#Hex_notation>
|
||||
* @param s escaped string
|
||||
|
|
@ -88,6 +100,87 @@ export function unescapeString(s: string): string {
|
|||
return s;
|
||||
}
|
||||
|
||||
/** 0000 … FFFF */
|
||||
export function hexQuad(n: number): string {
|
||||
if (n < 0x0000 || n > 0xFFFF) {
|
||||
throw RangeError(`${n} not in [0x0000,0xFFFF]`);
|
||||
}
|
||||
return n.toString(16).padStart(4, '0');
|
||||
}
|
||||
|
||||
/** 00000000 … FFFFFFFF */
|
||||
export function hexOcts(n: number): string {
|
||||
if (n < 0x0000 || n > 0xFFFFFFFF) {
|
||||
throw RangeError(`${n} not in [0x00000000,0xFFFFFFFF]`);
|
||||
}
|
||||
return n.toString(16).padStart(8, '0');
|
||||
}
|
||||
|
||||
/** escape one char for regex in \uXXXX form */
|
||||
function escapeRegexChar(ch: string) {
|
||||
const code = ch.codePointAt(0);
|
||||
if (code <= 0xFFFF) {
|
||||
return '\\u' + hexQuad(code);
|
||||
} else {
|
||||
return '\\U' + hexOcts(code);
|
||||
}
|
||||
}
|
||||
|
||||
/** chars that must be escaped: syntax, C0 + C1 controls */
|
||||
const REGEX_SYNTAX_CHAR = /^[\u0000-\u001F\u007F-\u009F{}\[\]\\?.^$*()/-]$/;
|
||||
|
||||
function escapeRegexCharIfSyntax(ch: string) {
|
||||
// escape if syntax or not valid
|
||||
if (REGEX_SYNTAX_CHAR.test(ch) || !isValidUnicode(ch.codePointAt(0))) {
|
||||
return escapeRegexChar(ch);
|
||||
} else {
|
||||
return ch; // leave unescaped
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Unescape one codepoint to \u or \U format
|
||||
* @param hex one codepoint in hex, such as '0127'
|
||||
* @returns the unescaped codepoint
|
||||
*/
|
||||
function regexOne(hex: string): string {
|
||||
const unescaped = unescapeOne(hex);
|
||||
// re-escape as 16 or 32 bit code units
|
||||
return Array.from(unescaped).map(ch => escapeRegexCharIfSyntax(ch)).join('');
|
||||
}
|
||||
/**
|
||||
* Unescapes a string according to UTS#18§1.1, see <https://www.unicode.org/reports/tr18/#Hex_notation>
|
||||
* @param s escaped string
|
||||
* @returns
|
||||
*/
|
||||
export function unescapeStringToRegex(s: string): string {
|
||||
if(!s) {
|
||||
return s;
|
||||
}
|
||||
try {
|
||||
/**
|
||||
* process one regex match
|
||||
* @param str ignored
|
||||
* @param matched the entire match such as '0127' or '22 22'
|
||||
* @returns the unescaped match
|
||||
*/
|
||||
function processMatch(str: string, matched: string) : string {
|
||||
const codepoints = matched.split(' ');
|
||||
const unescaped = codepoints.map(regexOne);
|
||||
return unescaped.join('');
|
||||
}
|
||||
s = s.replaceAll(MATCH_HEX_ESCAPE, processMatch);
|
||||
} catch(e) {
|
||||
if (e instanceof RangeError) {
|
||||
throw new UnescapeError(`Out of range while unescaping '${s}': ${e.message}`, { cause: e });
|
||||
/* c8 ignore next 3 */
|
||||
} else {
|
||||
throw e; // pass through some other error
|
||||
}
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
/** True if this string *could* be a UTF-32 single char */
|
||||
export function
|
||||
isOneChar(value: string) : boolean {
|
||||
|
|
|
|||
|
|
@ -92,6 +92,10 @@ describe('Test of Pattern Parsers', () => {
|
|||
`Give me \\m{a} and \\m{c}, or \\m{.}.`, markers),
|
||||
`Give me \uFFFF\u0008\u0001 and \uFFFF\u0008\u0003, or \uFFFF\u0008\uD7FF.`
|
||||
);
|
||||
assert.equal(MarkerParser.toSentinelString(
|
||||
`Give me \\m{a} and \\m{c}, or \\m{.}.`, markers, true),
|
||||
`Give me \\uffff\\u0008\\u0001 and \\uffff\\u0008\\u0003, or ${MarkerParser.ANY_MARKER_MATCH}.`
|
||||
);
|
||||
assert.throws(() =>
|
||||
MarkerParser.toSentinelString(
|
||||
`Want to see something funny? \\m{zzz}`, // out of range
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import 'mocha';
|
||||
import {assert} from 'chai';
|
||||
import {unescapeString, UnescapeError, isOneChar, toOneChar, unescapeOneQuadString, BadStringAnalyzer, isValidUnicode, describeCodepoint, isPUA, BadStringType} from '../../src/util/util.js';
|
||||
import {unescapeString, UnescapeError, isOneChar, toOneChar, unescapeOneQuadString, BadStringAnalyzer, isValidUnicode, describeCodepoint, isPUA, BadStringType, unescapeStringToRegex, unescapeQuadString} from '../../src/util/util.js';
|
||||
|
||||
describe('test UTF32 functions()', function() {
|
||||
it('should properly categorize strings', () => {
|
||||
|
|
@ -57,16 +57,40 @@ describe('test unescapeString()', function() {
|
|||
});
|
||||
});
|
||||
|
||||
describe('test unescapeRegex()', () => {
|
||||
it("should correctly handle 1..6 char escapes", function() {
|
||||
assert.equal(unescapeStringToRegex('\\u{9}'), '\\u0009'); // TAB
|
||||
assert.equal(unescapeStringToRegex('\\u{5b}'), '\\u005b'); // [
|
||||
assert.equal(unescapeStringToRegex('\\u{005b}'), '\\u005b'); // [
|
||||
assert.equal(unescapeStringToRegex('\\u{4a}'), 'J'); // J
|
||||
assert.equal(unescapeStringToRegex('\\u{8a}'), '\\u008a'); // J
|
||||
assert.equal(unescapeStringToRegex('\\u{3c8}'), 'ψ'); // ψ
|
||||
assert.equal(unescapeStringToRegex('\\u{304B}'), 'か'); // か
|
||||
assert.equal(unescapeStringToRegex('\\u{ffff}'), '\\uffff'); // noncharacter
|
||||
assert.equal(unescapeStringToRegex('\\u{1e109}'), '𞄉'); // 𞄉
|
||||
assert.equal(unescapeStringToRegex('\\u{1ffff}'), '\\U0001ffff'); // nonchar
|
||||
assert.equal(unescapeStringToRegex('\\u{10fff0}'), '\u{10fff0}'); // Plane 16 Private Use
|
||||
assert.equal(unescapeStringToRegex('\\u{10ffff}'), '\\U0010ffff'); // nonchar
|
||||
});
|
||||
});
|
||||
|
||||
describe('test unescapeOneQuadString()', () => {
|
||||
it('should be able to convert', () => {
|
||||
// testing that `\u0127` is unescaped correctly (to U+0127: 'ħ')
|
||||
assert.equal(unescapeOneQuadString('\\u0127'), '\u{0127}');
|
||||
assert.equal(unescapeOneQuadString('\\U0010FFF0'), '\u{10fff0}');
|
||||
// test the fail cases
|
||||
});
|
||||
it('should fail when it needs to fail', () => {
|
||||
assert.throws(() => unescapeOneQuadString(null), null);
|
||||
assert.throws(() => unescapeOneQuadString('\uFFFFFFFFFFFF'));
|
||||
});
|
||||
const PAIRED=`\\uD838\\uDD09`;
|
||||
it('test of paired surrogates ${UNPAIRED}', () => {
|
||||
const s = unescapeQuadString(PAIRED);
|
||||
assert.equal(s, '\u{1e109}');
|
||||
assert.equal(s, '\u{d838}\u{dd09}');
|
||||
});
|
||||
});
|
||||
|
||||
function titleize(o : any) {
|
||||
|
|
|
|||
|
|
@ -19,6 +19,12 @@ namespace core {
|
|||
namespace ldml {
|
||||
|
||||
|
||||
// the 'prefix part' of a regex marker sequence, followed by RAW_PREFIX or REGEX_ANY_MATCH
|
||||
const std::u32string REGEX_PREFIX = U"\\uffff\\u0008";
|
||||
const std::u32string RAW_PREFIX = U"\uffff\u0008";
|
||||
const std::u32string REGEX_ANY_MATCH = U"[\\u0001-\\ud7fe]";
|
||||
const KMX_DWORD LDML_MARKER_NO_INDEX = 0; // TODO: move to .ts
|
||||
|
||||
// string manipulation
|
||||
|
||||
/** internal function to normalize with a specified mode */
|
||||
|
|
@ -51,52 +57,51 @@ bool normalize_nfd(std::u16string &str) {
|
|||
return normalize(nfd, str, status);
|
||||
}
|
||||
|
||||
bool normalize_nfd_markers_segment(std::u16string &str, marker_map &map, marker_encoding encoding) {
|
||||
std::u32string rstr = km::core::kmx::u16string_to_u32string(str);
|
||||
if(!normalize_nfd_markers_segment(rstr, map, encoding)) {
|
||||
return false;
|
||||
} else {
|
||||
str = km::core::kmx::u32string_to_u16string(rstr);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
static void add_back_markers(std::u32string &str, const std::u32string &src, const marker_map &map, marker_encoding encoding) {
|
||||
static void add_back_markers(std::u32string &str, const std::u32string &src, marker_map &map, marker_encoding encoding) {
|
||||
// need to reconstitute.
|
||||
marker_map map2(map); // make a copy of the map
|
||||
// clear the string
|
||||
str.clear();
|
||||
// add the end-of-text marker
|
||||
{
|
||||
const auto ch = MARKER_BEFORE_EOT;
|
||||
const auto m = map2.find(ch);
|
||||
if (m != map2.end()) {
|
||||
for (auto q = (m->second).rbegin(); q < (m->second).rend(); q++) {
|
||||
prepend_marker(str, *q, encoding);
|
||||
}
|
||||
map2.erase(ch); // remove it
|
||||
}
|
||||
// iterator over the marker map
|
||||
auto marki = map2.rbegin();
|
||||
// number of markers left to process
|
||||
size_t processed_count = map2.size();
|
||||
|
||||
// add any end-of-text markers
|
||||
while(marki != map2.rend() && marki->first == MARKER_BEFORE_EOT) {
|
||||
prepend_marker(str, marki->second, encoding);
|
||||
processed_count--;
|
||||
marki->second = 0; // mark as done
|
||||
marki++;
|
||||
}
|
||||
|
||||
// go from end to beginning of string
|
||||
for (auto p = src.rbegin(); p != src.rend(); p++) {
|
||||
const auto ch = *p;
|
||||
str.insert(0, 1, ch); // prepend
|
||||
|
||||
const auto m = map2.find(ch);
|
||||
if (m != map2.end()) {
|
||||
for (auto q = (m->second).rbegin(); q < (m->second).rend(); q++) {
|
||||
prepend_marker(str, *q, encoding);
|
||||
// add the markers at the end of the list first.
|
||||
for (; marki != map2.rend() && marki->first == ch; marki++) {
|
||||
if (marki->second != 0) {
|
||||
// set to '0' if already applied
|
||||
prepend_marker(str, marki->second, encoding);
|
||||
marki->second = 0; // mark as already applied
|
||||
processed_count--;
|
||||
}
|
||||
}
|
||||
|
||||
// now, add any out of order markers.
|
||||
for (auto marki2 = marki; marki2 != map2.rend(); marki2++) {
|
||||
if (marki2->second != 0 && marki2->first == ch) {
|
||||
prepend_marker(str, marki2->second, encoding);
|
||||
marki2->second = 0; // mark as already applied
|
||||
processed_count--;
|
||||
}
|
||||
map2.erase(ch); // remove it
|
||||
}
|
||||
}
|
||||
assert(processed_count == 0); // assert that we consumed all marks
|
||||
}
|
||||
|
||||
/**
|
||||
* TODO-LDML:
|
||||
* - doesn't support >1 marker per char - may need a set instead of a map!
|
||||
* - ideally this should be used on a normalization safe subsequence
|
||||
*/
|
||||
bool normalize_nfd_markers_segment(std::u32string &str, marker_map &map, marker_encoding encoding) {
|
||||
/** original string, but no markers */
|
||||
std::u32string str_unmarked = remove_markers(str, map, encoding);
|
||||
|
|
@ -116,16 +121,160 @@ bool normalize_nfd_markers_segment(std::u32string &str, marker_map &map, marker_
|
|||
return true; // all OK
|
||||
}
|
||||
|
||||
bool normalize_nfd_markers(std::u16string &str, marker_encoding encoding) {
|
||||
marker_map m;
|
||||
// TODO-LDML: split segments
|
||||
return normalize_nfd_markers_segment(str, m, encoding);
|
||||
/**
|
||||
* @param i iteration point. will always advance unless at end
|
||||
* @param end end of input
|
||||
* @returns marker number, or LDML_MARKER_NO_INDEX if no marker (and i will be unmoved)
|
||||
*/
|
||||
marker_num parse_next_marker(std::u32string::const_iterator &i, const std::u32string::const_iterator &end, marker_encoding encoding) {
|
||||
if (i == end) {
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
const auto &lookfor_str = (encoding == regex_sentinel) ? REGEX_PREFIX : RAW_PREFIX;
|
||||
std::u32string rest(i, end);
|
||||
if (rest.length() <= lookfor_str.length()) { // <= because we need at least 1 char for the rest of the marker payload
|
||||
// input too short
|
||||
i++;
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
if (0 != rest.compare(0, lookfor_str.length(), lookfor_str)) {
|
||||
// advance past initial char
|
||||
i++;
|
||||
return LDML_MARKER_NO_INDEX; // prefix mismatch
|
||||
}
|
||||
i += lookfor_str.length(); // matches, so advance
|
||||
|
||||
// handle the plain_sentinel option here
|
||||
if (encoding == plain_sentinel) {
|
||||
marker_num marker_no = *(i++); // advance past marker no
|
||||
assert(marker_no >= LDML_MARKER_MIN_INDEX && marker_no <= LDML_MARKER_ANY_INDEX);
|
||||
return marker_no;
|
||||
}
|
||||
|
||||
assert(encoding == regex_sentinel);
|
||||
|
||||
// TODO-LDML: could change this from iterator to using 'rest'
|
||||
if (*i == U'\\') {
|
||||
// single marker
|
||||
if (++i == end) {
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
assert(*i == U'u');
|
||||
if (++i == end) {
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
km_core_usv markno[4];
|
||||
|
||||
markno[0] = *(i++);
|
||||
if (i == end) {
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
markno[1] = *(i++);
|
||||
if (i == end) {
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
markno[2] = *(i++);
|
||||
if (i == end) {
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
markno[3] = *(i++);
|
||||
auto marker_no = parse_hex_quad(markno);
|
||||
assert(marker_no >= LDML_MARKER_MIN_INDEX && marker_no <= LDML_MARKER_MAX_INDEX);
|
||||
return marker_no;
|
||||
} else if (*i == REGEX_ANY_MATCH.at(0)) { // '['
|
||||
std::u32string rest2(i, end); // TODO-LDML: could use 'rest' above
|
||||
if (rest2.length() < REGEX_ANY_MATCH.length()) {
|
||||
// not enough left so it can't match, so continue
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
i += REGEX_ANY_MATCH.length();
|
||||
return LDML_MARKER_ANY_INDEX;
|
||||
}
|
||||
return LDML_MARKER_NO_INDEX;
|
||||
}
|
||||
|
||||
bool normalize_nfd_markers(std::u32string &str, marker_encoding encoding) {
|
||||
marker_map m;
|
||||
// TODO-LDML: split segments
|
||||
return normalize_nfd_markers_segment(str, m, encoding);
|
||||
// quick check - don't bother if the string is empty
|
||||
if(str.empty()) return true;
|
||||
|
||||
// we're going to need an NFD normalizer
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
const icu::Normalizer2 *nfd = icu::Normalizer2::getNFDInstance(status);
|
||||
if (!UASSERT_SUCCESS(status)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* output string, we'll accumulate the normalized string here
|
||||
*/
|
||||
std::u32string out;
|
||||
/**
|
||||
* this is the beginning of the current segment to process.
|
||||
* it will also be the beginning of the string OR the end of the previous segment.
|
||||
*/
|
||||
std::u32string::const_iterator seg_start = str.begin();
|
||||
/** end of the current segment. This will be == seg_start unless a new segment is identified. */
|
||||
std::u32string::const_iterator seg_end = str.begin();
|
||||
/** iterator through this loop */
|
||||
std::u32string::const_iterator i = str.begin();
|
||||
|
||||
// now we'll loop through looking for normalization-safe subsegments of [seg_start, seg_end)
|
||||
// For example (sentinel mode) the following would be segments:
|
||||
// - A
|
||||
// - E\u0320\u0302
|
||||
// - U\u0320\m{marker}\u0300
|
||||
//
|
||||
// The marker will typically 'look' like an NFD-safe boundary, but it's not! It's part of the
|
||||
// subsegment, we want to skip over it. This is why we use parse_next_marker to look-ahead to see
|
||||
// if there is actually a marker under the iterator.
|
||||
//
|
||||
// We don't assume that the marker sentinel or regex appears as an NFD boundary, that is why
|
||||
// the parse function and the nfd function are called in parallel.
|
||||
|
||||
do {
|
||||
|
||||
// First, check if there's a marker.
|
||||
|
||||
// temporary iterator so we don't move 'i' unnecessarily. points to end of marker sequence.
|
||||
std::u32string::const_iterator marker_end = i;
|
||||
// true if marker detected
|
||||
bool have_marker = parse_next_marker(marker_end, str.end(), encoding) != LDML_MARKER_NO_INDEX;
|
||||
|
||||
|
||||
// Now, categorize the string. Is there a segment boundary BEFORE 'i'?
|
||||
if (i == str.end()) {
|
||||
// end of string, mark as the end of a segment
|
||||
// this will cause the final segment to be processed and the loop exitted.
|
||||
seg_end = i;
|
||||
} else if (nfd->hasBoundaryBefore(*i) && !have_marker) {
|
||||
// 'i' is the beginning of an NFD safe boundary (such as a base character).
|
||||
// but it's also not on a marker (which would be included in the segment)
|
||||
seg_end = i;
|
||||
// we also need to advance so we can pick up the next char.
|
||||
i++;
|
||||
} else if (have_marker) {
|
||||
// it's actually a marker. so, advance over it.
|
||||
i = marker_end;
|
||||
} else {
|
||||
// some other non boundary char.
|
||||
// advance without further drama
|
||||
i++;
|
||||
}
|
||||
|
||||
// Finally, process any identified segment.
|
||||
if (seg_end != seg_start) { // is the segment non-empty?
|
||||
std::u32string segment(seg_start, seg_end);
|
||||
marker_map m;
|
||||
if (!normalize_nfd_markers_segment(segment, m, encoding)) {
|
||||
return false;
|
||||
}
|
||||
out.append(segment);
|
||||
seg_start = seg_end;
|
||||
}
|
||||
} while(seg_end != str.end()); // repeat until the last codepoint has been processed in a segment
|
||||
// update output string
|
||||
str = out;
|
||||
return true;
|
||||
}
|
||||
|
||||
// TODO-LDML: cleanup
|
||||
|
|
@ -176,23 +325,15 @@ prepend_marker(std::u32string &str, marker_num marker, marker_encoding encoding)
|
|||
assert(encoding == regex_sentinel);
|
||||
if (marker == LDML_MARKER_ANY_INDEX) {
|
||||
// recreate the regex from back to front
|
||||
str.insert(0, 1, U']');
|
||||
prepend_hex_quad(str, LDML_MARKER_MAX_INDEX);
|
||||
str.insert(0, 1, U'u');
|
||||
str.insert(0, 1, U'\\');
|
||||
str.insert(0, 1, U'-');
|
||||
prepend_hex_quad(str, LDML_MARKER_MIN_INDEX);
|
||||
str.insert(0, 1, U'u');
|
||||
str.insert(0, 1, U'\\');
|
||||
str.insert(0, 1, U'[');
|
||||
str.insert(0, 1, LDML_MARKER_CODE);
|
||||
str.insert(0, 1, LDML_UC_SENTINEL);
|
||||
str.insert(0, REGEX_ANY_MATCH);
|
||||
str.insert(0, REGEX_PREFIX);
|
||||
} else {
|
||||
// add hex part
|
||||
prepend_hex_quad(str, marker);
|
||||
// add static part
|
||||
km_core_usv markstr[] = {LDML_UC_SENTINEL, LDML_MARKER_CODE, u'\\', u'u'};
|
||||
str.insert(0, markstr, 4);
|
||||
km_core_usv markstr[] = {u'\\', u'u'};
|
||||
str.insert(0, markstr, 2);
|
||||
str.insert(0, REGEX_PREFIX);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -239,175 +380,77 @@ KMX_DWORD parse_hex_quad(const km_core_usv hex_str[]) {
|
|||
}
|
||||
|
||||
/** add the list to the map */
|
||||
void add_markers_to_map(marker_map &markers, char32_t marker_ch, const marker_list &list) {
|
||||
auto rep = markers.emplace(marker_ch, list);
|
||||
if (!rep.second) {
|
||||
// already existed.
|
||||
auto existing = rep.first;
|
||||
// append all additional ones
|
||||
for(auto m = list.begin(); m < list.end(); m++) {
|
||||
existing->second.emplace_back(*m);
|
||||
}
|
||||
static void add_markers_to_map(marker_map &markers, char32_t marker_ch, const marker_list &list) {
|
||||
for (auto i = list.begin(); i < list.end(); i++) {
|
||||
// marker_ch is duplicate, but keeps the structure more shallow.
|
||||
markers.emplace_back(marker_ch, *i);
|
||||
}
|
||||
}
|
||||
|
||||
std::u32string remove_markers(const std::u32string &str, marker_map *markers, marker_encoding encoding) {
|
||||
/**
|
||||
* Add any markers, if needed
|
||||
* @param markers marker map or nullptr
|
||||
* @param last the 'last' parameter past the prior parsing
|
||||
* @param end end of the input string
|
||||
*/
|
||||
inline void
|
||||
add_pending_markers(
|
||||
marker_map *markers,
|
||||
marker_list &last_markers,
|
||||
const std::u32string::const_iterator &last,
|
||||
const std::u32string::const_iterator &end) {
|
||||
if(markers == nullptr || last_markers.empty()) {
|
||||
return;
|
||||
}
|
||||
char32_t marker_ch;
|
||||
if (last == end) {
|
||||
marker_ch = MARKER_BEFORE_EOT;
|
||||
} else {
|
||||
marker_ch = *last;
|
||||
}
|
||||
add_markers_to_map(*markers, marker_ch, last_markers);
|
||||
last_markers.clear(); // mark as already recorded
|
||||
}
|
||||
|
||||
std::u32string
|
||||
remove_markers(const std::u32string &str, marker_map *markers, marker_encoding encoding) {
|
||||
std::u32string out;
|
||||
auto i = str.begin();
|
||||
auto last = i;
|
||||
marker_list last_markers;
|
||||
for (i = find(i, str.end(), LDML_UC_SENTINEL); i != str.end(); i = find(i, str.end(), LDML_UC_SENTINEL)) {
|
||||
// append any prefix (from prior pos'n to here)
|
||||
out.append(last, i);
|
||||
|
||||
// #1: LDML_UC_SENTINEL (what we searched for)
|
||||
assert(*i == LDML_UC_SENTINEL); // assert that find() worked
|
||||
i++;
|
||||
last = i;
|
||||
if (i == str.end()) {
|
||||
break; // hit end
|
||||
}
|
||||
|
||||
// #2 LDML_MARKER_CODE
|
||||
if (*i != LDML_MARKER_CODE) {
|
||||
continue; // can't process this, get out
|
||||
}
|
||||
i++;
|
||||
last = i;
|
||||
if (i == str.end()) {
|
||||
break; // hit end
|
||||
}
|
||||
|
||||
KMX_DWORD marker_no;
|
||||
if (encoding == plain_sentinel) {
|
||||
// #3 marker number
|
||||
marker_no = *i;
|
||||
i++; // if end, we'll break out of the loop
|
||||
auto last = str.begin(); // points to the part of the string after the last matched marker
|
||||
for (auto i = str.begin(); i != str.end();) {
|
||||
auto marker_no = parse_next_marker(i, str.end(), encoding);
|
||||
if (marker_no == LDML_MARKER_NO_INDEX) {
|
||||
// add any markers found before this entry, but only if there is intervening
|
||||
// text. This prevents the sentinel or the '\u' from becoming the attachment char.
|
||||
if (i != last) {
|
||||
add_pending_markers(markers, last_markers, last, str.end());
|
||||
out.append(last, i); // append any non-marker text since the end of the last marker
|
||||
last = i; // advance over text we've already appended
|
||||
}
|
||||
} else {
|
||||
assert(encoding == regex_sentinel);
|
||||
// is it an escape or a range?
|
||||
if (*i == U'\\') {
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(*i == U'u');
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
km_core_usv markno[4];
|
||||
assert(marker_no >= LDML_MARKER_MIN_INDEX && marker_no <= LDML_MARKER_ANY_INDEX);
|
||||
// The marker number is good, add it to the list
|
||||
if (marker_no >= LDML_MARKER_MIN_INDEX && markers != nullptr) {
|
||||
// add it to the list
|
||||
last_markers.emplace_back(marker_no);
|
||||
}
|
||||
last = i; // skip over marker
|
||||
}
|
||||
}
|
||||
|
||||
markno[0] = *(i++);
|
||||
if (i == str.end()) {
|
||||
break;
|
||||
}
|
||||
markno[1] = *(i++);
|
||||
if (i == str.end()) {
|
||||
break;
|
||||
}
|
||||
markno[2] = *(i++);
|
||||
if (i == str.end()) {
|
||||
break;
|
||||
}
|
||||
markno[3] = *(i++);
|
||||
marker_no = parse_hex_quad(markno);
|
||||
assert (marker_no != 0); // illegal marker number
|
||||
} else if (*i == U'[') {
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(*i == U'\\');
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(*i == U'u');
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(xdigitval(*i) != -1);
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(xdigitval(*i) != -1);
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(xdigitval(*i) != -1);
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(xdigitval(*i) != -1);
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(*i == U'-');
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(*i == U'\\');
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(*i == U'u');
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(xdigitval(*i) != -1);
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(xdigitval(*i) != -1);
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(xdigitval(*i) != -1);
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(xdigitval(*i) != -1);
|
||||
if (++i == str.end()) {
|
||||
break;
|
||||
}
|
||||
assert(*i == U']');
|
||||
i++;
|
||||
marker_no = LDML_MARKER_ANY_INDEX;
|
||||
} else {
|
||||
assert(*i == U'\\' || *i == U'['); // error.
|
||||
marker_no = 0; // error, don't record
|
||||
}
|
||||
}
|
||||
assert(marker_no >= LDML_MARKER_MIN_INDEX && marker_no <= LDML_MARKER_ANY_INDEX);
|
||||
// The marker number is good, add it to the list
|
||||
last = i;
|
||||
// record the marker
|
||||
if (marker_no >= LDML_MARKER_MIN_INDEX && markers != nullptr) {
|
||||
// add it to the list
|
||||
last_markers.emplace_back(marker_no);
|
||||
char32_t marker_ch;
|
||||
if (i == str.end()) {
|
||||
// Hit end, so mark it as the end
|
||||
marker_ch = MARKER_BEFORE_EOT;
|
||||
} else if (*i == LDML_UC_SENTINEL) {
|
||||
// it's another marker (presumably)
|
||||
continue; // loop around
|
||||
} else {
|
||||
marker_ch = *i;
|
||||
}
|
||||
add_markers_to_map(*markers, marker_ch, last_markers);
|
||||
last_markers.clear(); // mark as already recorded
|
||||
}
|
||||
}
|
||||
// get the suffix between the last marker and the end
|
||||
// add any remaining pending markers.
|
||||
// if last == str.end() then this wil be MARKER_BEFORE_EOT
|
||||
// otherwise it will be the glue character
|
||||
add_pending_markers(markers, last_markers, last, str.end());
|
||||
// get the suffix between the last marker and the end (could be nothing)
|
||||
out.append(last, str.end());
|
||||
if (!last_markers.empty() && markers != nullptr) {
|
||||
// we had markers but couldn't find the base.
|
||||
// it's possible that there was a malformed UC_SENTINEL string in between.
|
||||
// Add it to the end.
|
||||
add_markers_to_map(*markers, MARKER_BEFORE_EOT, last_markers);
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
|
||||
|
||||
// --- end namespaces
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -48,11 +48,14 @@ enum marker_encoding {
|
|||
/** a marker ID (1-based) */
|
||||
typedef KMX_DWORD marker_num;
|
||||
|
||||
/** list of markers */
|
||||
/** list of marker numbers */
|
||||
typedef std::deque<marker_num> marker_list;
|
||||
|
||||
/** map from following-char to marker numbers. */
|
||||
typedef std::map<char32_t, marker_list> marker_map;
|
||||
/** map from one char to one entry */
|
||||
typedef std::pair<char32_t, marker_num> marker_entry;
|
||||
|
||||
/** map from following-char to marker numbers, in front to back order */
|
||||
typedef std::deque<marker_entry> marker_map;
|
||||
|
||||
/** Normalize a u32string inplace to NFD. @return false on failure */
|
||||
bool normalize_nfd(std::u32string &str);
|
||||
|
|
@ -63,9 +66,7 @@ bool normalize_nfd(std::u16string &str);
|
|||
* @return false on failure
|
||||
**/
|
||||
bool normalize_nfd_markers_segment(std::u32string &str, marker_map &markers, marker_encoding encoding = plain_sentinel);
|
||||
bool normalize_nfd_markers_segment(std::u16string &str, marker_map &markers, marker_encoding encoding = plain_sentinel);
|
||||
bool normalize_nfd_markers(std::u32string &str, marker_encoding encoding = plain_sentinel);
|
||||
bool normalize_nfd_markers(std::u16string &str, marker_encoding encoding = plain_sentinel);
|
||||
|
||||
// /** Normalize a u32string inplace to NFC, retaining markers.
|
||||
// * @param markers will be populated with marker chars
|
||||
|
|
|
|||
|
|
@ -283,19 +283,23 @@ ldml_processor::process_key_string(km_core_state *state, const std::u16string &k
|
|||
|
||||
size_t ldml_processor::process_output(km_core_state *state, const std::u32string &str, ldml::transforms *with_transforms) const {
|
||||
std::u32string nfd_str = str;
|
||||
assert(ldml::normalize_nfd_markers(nfd_str)); // TODO-LDML: else fail?
|
||||
// Note:
|
||||
// The normalize functions have assert and Debuglog at the bottom.
|
||||
// so we do not need to assert the status here unless we're going to do something
|
||||
// different with control flow.
|
||||
(void)ldml::normalize_nfd_markers(nfd_str);
|
||||
|
||||
// extract context string, in NFD
|
||||
std::u32string old_ctxtstr_nfd;
|
||||
(void)context_to_string(state, old_ctxtstr_nfd, true);
|
||||
assert(ldml::normalize_nfd_markers(old_ctxtstr_nfd)); // TODO-LDML: else fail?
|
||||
(void)ldml::normalize_nfd_markers(old_ctxtstr_nfd);
|
||||
|
||||
// context string in NFD
|
||||
std::u32string ctxtstr;
|
||||
(void)context_to_string(state, ctxtstr, true); // with markers
|
||||
// add the newly added key output to ctxtstr
|
||||
ctxtstr.append(nfd_str);
|
||||
assert(ldml::normalize_nfd_markers(ctxtstr)); // TODO-LDML: else fail?
|
||||
|
||||
(void)ldml::normalize_nfd_markers(ctxtstr);
|
||||
/** transform output string */
|
||||
std::u32string outputString;
|
||||
/** how many chars of the ctxtstr to replace */
|
||||
|
|
@ -305,9 +309,7 @@ size_t ldml_processor::process_output(km_core_state *state, const std::u32string
|
|||
|
||||
if(with_transforms != nullptr) {
|
||||
matchedContext = with_transforms->apply(ctxtstr, outputString);
|
||||
} else {
|
||||
// no transforms, no output
|
||||
}
|
||||
} // else: no transforms, no output
|
||||
|
||||
// Short Circuit: if no transforms matched, and no new text is being output,
|
||||
// just return.
|
||||
|
|
@ -318,27 +320,13 @@ size_t ldml_processor::process_output(km_core_state *state, const std::u32string
|
|||
// drop last 'matchedContext':
|
||||
ctxtstr.resize(ctxtstr.length() - matchedContext);
|
||||
ctxtstr.append(outputString); // TODO-LDML: should be able to do a normalization-safe append here.
|
||||
{
|
||||
const auto normalize_ok = ldml::normalize_nfd_markers(ctxtstr);
|
||||
assert(normalize_ok);
|
||||
if(!normalize_ok) {
|
||||
DebugLog("ldml_processor::process_output: failed ldml::normalize_nfd_markers(ctxtstr)");
|
||||
}
|
||||
}
|
||||
(void)ldml::normalize_nfd_markers(ctxtstr);
|
||||
|
||||
// Ok. We've done all the happy manipulations.
|
||||
|
||||
/** NFD w/ markers */
|
||||
std::u32string ctxtstr_cleanedup = ctxtstr;
|
||||
{
|
||||
const auto normalize_ok = ldml::normalize_nfd_markers(ctxtstr_cleanedup);
|
||||
assert(normalize_ok);
|
||||
if(!normalize_ok) {
|
||||
DebugLog("ldml_processor::process_output: failed ldml::normalize_nfd_markers(ctxtstr_cleanedup)");
|
||||
}
|
||||
}
|
||||
|
||||
assert(ldml::normalize_nfd_markers(ctxtstr_cleanedup));
|
||||
(void)ldml::normalize_nfd_markers(ctxtstr_cleanedup);
|
||||
|
||||
// find common prefix.
|
||||
// For example, if the context previously had "aaBBBBB" and it is changing to "aaCCC" then we will have:
|
||||
|
|
@ -346,6 +334,32 @@ size_t ldml_processor::process_output(km_core_state *state, const std::u32string
|
|||
// - new_ctxtstr_changed = "CCC"
|
||||
// So the BBBBB needs to be removed and then CCC added.
|
||||
auto ctxt_prefix = mismatch(old_ctxtstr_nfd.begin(), old_ctxtstr_nfd.end(), ctxtstr_cleanedup.begin(), ctxtstr_cleanedup.end());
|
||||
|
||||
// handle a special case where we're simply changing from one marker to another.
|
||||
// Example:
|
||||
// 0. old_ctxtstr_changed ends with … U+FFFF U+0008 | U+0001 …
|
||||
// 1. ctxtstr_cleanedup ends with … U+FFFF U+0008 | U+0002 …
|
||||
// Pipe symbol shows where the difference starts.
|
||||
// As you can see, the different starts in the MIDDLE of a marker sequence.
|
||||
// so, old_ctxtstr_changed will start with U+0001
|
||||
// and new_ctxtstr_changed will start with U+0002
|
||||
// remove_text will only be able to delete up to and through the U+0001
|
||||
// and it will need to emit a push_backspace(KM_CORE_BT_MARKER,…) due to the
|
||||
// marker change.
|
||||
// We can detect this because the unchanged_prefix will end with u+FFFF U+0008
|
||||
//
|
||||
// Oh, and yes, test case 'regex-test-8a-0' hits this.
|
||||
std::u32string common_prefix(old_ctxtstr_nfd.begin(), ctxt_prefix.first);
|
||||
if (common_prefix.length() >= 2) {
|
||||
auto iter = common_prefix.rbegin();
|
||||
if (*(iter++) == LDML_MARKER_CODE && *(iter++) == UC_SENTINEL) {
|
||||
// adjust the iterator so that the "U+FFFF U+0008" is not a part of the common prefix.
|
||||
ctxt_prefix.first -= 2;
|
||||
ctxt_prefix.second += 2;
|
||||
// Now, old_ctxtstr_changed and new_ctxtstr_changed will start with U+FFFF U+0008 …
|
||||
}
|
||||
}
|
||||
|
||||
/** The part of the old string to be removed */
|
||||
std::u32string old_ctxtstr_changed(ctxt_prefix.first,old_ctxtstr_nfd.end());
|
||||
/** The new context to be added */
|
||||
|
|
@ -367,6 +381,8 @@ size_t ldml_processor::process_output(km_core_state *state, const std::u32string
|
|||
|
||||
void
|
||||
ldml_processor::remove_text(km_core_state *state, std::u32string &str, size_t length) {
|
||||
// str is the string to remove, so it should be at least as long as length
|
||||
assert(length <= str.length());
|
||||
/** track how many context items have been removed, via push_backspace() */
|
||||
size_t contextRemoved = 0;
|
||||
for (auto c = state->context().rbegin(); length > 0 && c != state->context().rend(); c++, contextRemoved++) {
|
||||
|
|
@ -382,23 +398,23 @@ ldml_processor::remove_text(km_core_state *state, std::u32string &str, size_t le
|
|||
// Cause prior char to be removed
|
||||
state->actions().push_backspace(KM_CORE_BT_CHAR, c->character);
|
||||
} else if (type == KM_CORE_BT_MARKER) {
|
||||
// It's a marker.
|
||||
// need to be able to drop 3 chars
|
||||
assert(length >= 3);
|
||||
length -= 3;
|
||||
state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker);
|
||||
// #3 - the marker.
|
||||
assert(lastCtx == c->marker);
|
||||
str.pop_back();
|
||||
length--;
|
||||
// #2 - the code
|
||||
assert(str.back() == LDML_MARKER_CODE);
|
||||
str.pop_back();
|
||||
length--;
|
||||
// #1 - the sentinel
|
||||
assert(str.back() == UC_SENTINEL);
|
||||
str.pop_back();
|
||||
// cause marker to be removed
|
||||
state->actions().push_backspace(KM_CORE_BT_MARKER, c->marker);
|
||||
length--;
|
||||
}
|
||||
}
|
||||
assert(length == 0);
|
||||
// now, pop the context items
|
||||
for (size_t i = 0; i < contextRemoved; i++) {
|
||||
// we don't pop during the above loop because the iterator gets confused
|
||||
|
|
|
|||
|
|
@ -519,9 +519,10 @@ transform_entry::init() {
|
|||
return false;
|
||||
}
|
||||
// TODO-LDML: if we have mapFrom, may need to do other processing.
|
||||
std::u16string patstr = km::core::kmx::u32string_to_u16string(fFrom);
|
||||
std::u32string from2 = fFrom;
|
||||
normalize_nfd_markers(from2, regex_sentinel);
|
||||
std::u16string patstr = km::core::kmx::u32string_to_u16string(from2);
|
||||
// normalize, including markers, for regex
|
||||
normalize_nfd_markers(patstr, regex_sentinel);
|
||||
UErrorCode status = U_ZERO_ERROR;
|
||||
/* const */ icu::UnicodeString patustr = icu::UnicodeString(patstr.data(), (int32_t)patstr.length());
|
||||
// add '$' to match to end
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@
|
|||
<!-- Note: displays is only used for keycap presentation -->
|
||||
<!-- this example is not required for this keyboard as we use the spacing
|
||||
modifiers -->
|
||||
<display output="\u0300" display="\u02CB" /> <!-- display combining grave as modifier letter
|
||||
<display output="\u{0300}" display="\u{02CB}" /> <!-- display combining grave as modifier letter
|
||||
grave ˋ -->
|
||||
<!-- Note: We discussed why the existing displayMap was used for display.
|
||||
rather than adding something closer to the key layout. 1. This way we can
|
||||
|
|
|
|||
|
|
@ -35,12 +35,12 @@
|
|||
<transforms type="simple">
|
||||
<transformGroup>
|
||||
<!-- from the spec: nod-Lana for ᩅᩫ᩶ -->
|
||||
<reorder from="\u1A60" order="127" />
|
||||
<reorder from="\u1A6B" order="42" />
|
||||
<reorder from="[\u1A75-\u1A79]" order="55" />
|
||||
<reorder before="\u1A6B" from="\u1A60\u1A45" order="10" />
|
||||
<reorder before="\u1A6B[\u1A75-\u1A79]" from="\u1A60\u1A45" order="10" />
|
||||
<reorder before="\u1A6B" from="\u1A60[\u1A75-\u1A79]\u1A45" order="10 55 10" />
|
||||
<reorder from="\u{1A60}" order="127" />
|
||||
<reorder from="\u{1A6B}" order="42" />
|
||||
<reorder from="[\u{1A75}-\u{1A79}]" order="55" />
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}\u{1A45}" order="10" />
|
||||
<reorder before="\u{1A6B}[\u{1A75}-\u{1A79}]" from="\u{1A60}\u{1A45}" order="10" />
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}[\u{1A75}-\u{1A79}]\u{1A45}" order="10 55 10" />
|
||||
</transformGroup>
|
||||
</transforms>
|
||||
</keyboard3>
|
||||
|
|
|
|||
|
|
@ -1,11 +1,11 @@
|
|||
#include "../../../src/ldml/ldml_transforms.hpp"
|
||||
#include "../../../src/ldml/ldml_markers.hpp"
|
||||
#include "../../../src/ldml/ldml_transforms.hpp"
|
||||
#include "kmx/kmx_plus.h"
|
||||
#include "kmx/kmx_xstring.h"
|
||||
#include "test_color.h"
|
||||
#include <iostream>
|
||||
#include <string>
|
||||
#include <test_assert.h>
|
||||
#include "test_color.h"
|
||||
|
||||
// TODO-LDML: normal asserts wern't working, so using some hacks.
|
||||
// #include "ldml_test_utils.hpp"
|
||||
|
|
@ -13,18 +13,19 @@
|
|||
// #include "debuglog.h"
|
||||
|
||||
#ifndef zassert_string_equal
|
||||
#define zassert_string_equal(actual, expected) \
|
||||
{ \
|
||||
if (actual != expected) { \
|
||||
std::wcerr << __FILE__ << ":" << __LINE__ << ": " << console_color::fg(console_color::BRIGHT_RED) << "got: " << km::core::kmx::Debug_UnicodeString(actual, 0) \
|
||||
<< " expected " << km::core::kmx::Debug_UnicodeString(expected, 1) << console_color::reset() << std::endl; \
|
||||
return EXIT_FAILURE; \
|
||||
} \
|
||||
#define zassert_string_equal(actual, expected) \
|
||||
{ \
|
||||
if (actual != expected) { \
|
||||
std::wcerr << __FILE__ << ":" << __LINE__ << ": " << console_color::fg(console_color::BRIGHT_RED) \
|
||||
<< "got: " << km::core::kmx::Debug_UnicodeString(actual, 0) << " expected " \
|
||||
<< km::core::kmx::Debug_UnicodeString(expected, 1) << console_color::reset() << std::endl; \
|
||||
return EXIT_FAILURE; \
|
||||
} \
|
||||
}
|
||||
#endif
|
||||
|
||||
#ifndef zassert_equal
|
||||
#define zassert_equal(actual, expected) \
|
||||
#define zassert_equal(actual, expected) \
|
||||
{ \
|
||||
if (actual != expected) { \
|
||||
std::wcerr << __FILE__ << ":" << __LINE__ << ": " << console_color::fg(console_color::BRIGHT_RED) << "got: " << actual \
|
||||
|
|
@ -34,33 +35,48 @@
|
|||
}
|
||||
#endif
|
||||
|
||||
|
||||
// needed for streaming operators
|
||||
#include "utfcodec.hpp"
|
||||
|
||||
using namespace km::core::ldml;
|
||||
using namespace km::core::kmx;
|
||||
|
||||
std::u32string marker_list_to_string(const marker_list &m) {
|
||||
void
|
||||
prepend_hex_oct(std::u32string &str, char32_t x) {
|
||||
for (auto i = 0; i < 8; i++) {
|
||||
KMX_DWORD remainder = x & 0xF; // get the last nibble
|
||||
const char32_t ch = remainder < 0xA ? U'0' + remainder : U'A' + (remainder - 0xA);
|
||||
str.insert(0, 1, ch); // prepend
|
||||
x >>= 4;
|
||||
}
|
||||
}
|
||||
|
||||
std::u32string
|
||||
marker_map_to_string(const marker_map &m) {
|
||||
std::u32string s;
|
||||
for (auto i = m.rbegin(); i < m.rend(); i++) {
|
||||
prepend_hex_quad(s, *i);
|
||||
prepend_hex_oct(s, i->first);
|
||||
s.insert(0, U"=U+");
|
||||
|
||||
prepend_hex_quad(s, i->second);
|
||||
s.insert(0, U" \\m0x");
|
||||
}
|
||||
return s;
|
||||
}
|
||||
|
||||
bool _assert_marker_list_equal(const char *f, int l, const marker_list a, const marker_list x) {
|
||||
if (a == x) return true;
|
||||
bool
|
||||
_assert_marker_map_equal(const char *f, int l, const marker_map a, const marker_map x) {
|
||||
if (a == x)
|
||||
return true;
|
||||
std::wcerr << f << ":" << l << ": " << console_color::fg(console_color::BRIGHT_RED);
|
||||
std::wcerr << "got: " << marker_list_to_string(a);
|
||||
std::wcerr << " expected: " << marker_list_to_string(x);
|
||||
std::wcerr << "got: " << marker_map_to_string(a);
|
||||
std::wcerr << " expected: " << marker_map_to_string(x);
|
||||
std::wcerr << console_color::reset() << std::endl;
|
||||
return false;
|
||||
}
|
||||
|
||||
#define assert_marker_list_equal(actual, expected) \
|
||||
if (!_assert_marker_list_equal(__FILE__, __LINE__, (actual), (expected))) \
|
||||
#define assert_marker_map_equal(actual, expected) \
|
||||
if (!_assert_marker_map_equal(__FILE__, __LINE__, (actual), (expected))) \
|
||||
return EXIT_FAILURE;
|
||||
|
||||
// using km::core::kmx::u16cmp;
|
||||
|
|
@ -222,7 +238,7 @@ test_reorder_standalone() {
|
|||
std::cout << __FILE__ << ":" << __LINE__ << " - element API test " << std::endl;
|
||||
// element test
|
||||
{
|
||||
element es(U'a', (80 << LDML_ELEM_FLAGS_ORDER_BITSHIFT) | LDML_ELEM_FLAGS_PREBASE); // tertiary -12, primary 80
|
||||
element es(U'a', (80 << LDML_ELEM_FLAGS_ORDER_BITSHIFT) | LDML_ELEM_FLAGS_PREBASE); // tertiary -12, primary 80
|
||||
std::cout << "es flags" << std::hex << es.get_flags() << std::dec << std::endl;
|
||||
// verify element metadata
|
||||
assert_equal(es.is_uset(), false);
|
||||
|
|
@ -386,19 +402,19 @@ test_reorder_standalone() {
|
|||
std::cout << __FILE__ << ":" << __LINE__ << " - trying roast #" << r << "=" << roast << std::endl;
|
||||
// try apply with string
|
||||
{
|
||||
std::cout << "- try apply(text, output)" << std::endl;
|
||||
std::u32string text = roast;
|
||||
std::u32string output;
|
||||
size_t len = tr.apply(text, output);
|
||||
if (len == 0) {
|
||||
std::cout << " (did not apply)" << std::endl;
|
||||
} else {
|
||||
std::cout << " applied, matchLen= " << len << std::endl;
|
||||
text.resize(text.size()-len); // shrink
|
||||
text.append(output);
|
||||
std::cout << " = " << text << std::endl;
|
||||
}
|
||||
zassert_string_equal(text, expect);
|
||||
std::cout << "- try apply(text, output)" << std::endl;
|
||||
std::u32string text = roast;
|
||||
std::u32string output;
|
||||
size_t len = tr.apply(text, output);
|
||||
if (len == 0) {
|
||||
std::cout << " (did not apply)" << std::endl;
|
||||
} else {
|
||||
std::cout << " applied, matchLen= " << len << std::endl;
|
||||
text.resize(text.size() - len); // shrink
|
||||
text.append(output);
|
||||
std::cout << " = " << text << std::endl;
|
||||
}
|
||||
zassert_string_equal(text, expect);
|
||||
}
|
||||
// try all-at-once
|
||||
{
|
||||
|
|
@ -435,7 +451,7 @@ test_reorder_standalone() {
|
|||
// special test
|
||||
{
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - special test " << std::endl;
|
||||
const std::u32string expect = U"\u1A21\u1A60\u1A45"; // this string shouldn't mutate at all.
|
||||
const std::u32string expect = U"\u1A21\u1A60\u1A45"; // this string shouldn't mutate at all.
|
||||
{
|
||||
std::u32string text = expect;
|
||||
tr.apply(text);
|
||||
|
|
@ -454,7 +470,6 @@ test_reorder_standalone() {
|
|||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
// this test case is also in XML form under 'k_201_*'
|
||||
int
|
||||
test_reorder_esk() {
|
||||
|
|
@ -516,34 +531,34 @@ test_reorder_esk() {
|
|||
// now actually test it
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - cases " << std::endl;
|
||||
const std::u32string orig_expect[] = {
|
||||
// 1short
|
||||
U"ax\u0305", // orig
|
||||
U"a\u0305x", // expect
|
||||
// 1short
|
||||
U"ax\u0305", // orig
|
||||
U"a\u0305x", // expect
|
||||
|
||||
// 2longer
|
||||
U"az\u0305x\u0332", // orig
|
||||
U"a\u0332\u0305xz", // expect
|
||||
// 2longer
|
||||
U"az\u0305x\u0332", // orig
|
||||
U"a\u0332\u0305xz", // expect
|
||||
};
|
||||
// TODO-LDML: move this into test code perhaps
|
||||
for (size_t r = 0; r < sizeof(orig_expect) / sizeof(orig_expect[0]); r+= 2) {
|
||||
for (size_t r = 0; r < sizeof(orig_expect) / sizeof(orig_expect[0]); r += 2) {
|
||||
const auto &orig = orig_expect[r + 0];
|
||||
const auto &expect = orig_expect[r + 1];
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - trying str #" << r+1 << "=" << orig << std::endl;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - trying str #" << r + 1 << "=" << orig << std::endl;
|
||||
// try apply with string
|
||||
{
|
||||
std::cout << "- try apply(text, output)" << std::endl;
|
||||
std::u32string text = orig;
|
||||
std::u32string output;
|
||||
size_t len = tr.apply(text, output);
|
||||
if (len == 0) {
|
||||
std::cout << " (did not apply)" << std::endl;
|
||||
} else {
|
||||
std::cout << " applied, matchLen= " << len << std::endl;
|
||||
text.resize(text.size()-len); // shrink
|
||||
text.append(output);
|
||||
std::cout << " = " << text << std::endl;
|
||||
}
|
||||
zassert_string_equal(text, expect);
|
||||
std::cout << "- try apply(text, output)" << std::endl;
|
||||
std::u32string text = orig;
|
||||
std::u32string output;
|
||||
size_t len = tr.apply(text, output);
|
||||
if (len == 0) {
|
||||
std::cout << " (did not apply)" << std::endl;
|
||||
} else {
|
||||
std::cout << " applied, matchLen= " << len << std::endl;
|
||||
text.resize(text.size() - len); // shrink
|
||||
text.append(output);
|
||||
std::cout << " = " << text << std::endl;
|
||||
}
|
||||
zassert_string_equal(text, expect);
|
||||
}
|
||||
// try all-at-once
|
||||
{
|
||||
|
|
@ -581,7 +596,8 @@ test_reorder_esk() {
|
|||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int test_map() {
|
||||
int
|
||||
test_map() {
|
||||
std::cout << "== " << __FUNCTION__ << std::endl;
|
||||
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " transform_entry::findIndex" << std::endl;
|
||||
|
|
@ -602,91 +618,96 @@ int test_map() {
|
|||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int test_strutils() {
|
||||
int
|
||||
test_strutils() {
|
||||
std::cout << "== " << __FUNCTION__ << std::endl;
|
||||
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " * remove_markers" << std::endl;
|
||||
|
||||
|
||||
{
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - basic test0" << std::endl;
|
||||
const std::u32string src = U"abc";
|
||||
const std::u32string dst = remove_markers(src);
|
||||
zassert_string_equal(dst, src); // unchanged
|
||||
zassert_string_equal(dst, src); // unchanged
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - basic test" << std::endl;
|
||||
const std::u32string src = U"abc";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
zassert_string_equal(dst, src); // unchanged
|
||||
zassert_string_equal(dst, src); // unchanged
|
||||
assert_equal(map.size(), 0);
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - marker test" << std::endl;
|
||||
const std::u32string src = U"6\U0000ffff\U00000008\U00000001e";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string src = U"6\U0000ffff\U00000008\U00000001e";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = U"6e";
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm = {{U'e', 0x1L}};
|
||||
assert_marker_map_equal(map, expm); // marker 1 @ e
|
||||
assert_equal(map.size(), 1);
|
||||
marker_list exp_e = { 0x1L };
|
||||
assert_marker_list_equal(map[U'e'], exp_e); // marker 1 @ e
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - bad0" << std::endl;
|
||||
const std::u32string src = U"6\U0000ffff\U00000008"; // missing trailing marker #
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = U"6";
|
||||
const std::u32string src = U"6\U0000ffff\U00000008"; // missing trailing marker #
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = src;
|
||||
zassert_string_equal(dst, expect);
|
||||
assert_equal(map.size(), 0);
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - bad1" << std::endl;
|
||||
const std::u32string src = U"6\U0000ffffq"; // missing code
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = U"6q";
|
||||
const std::u32string src = U"6\U0000ffffq"; // missing sentinel subtype
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = src; // 'q' removed
|
||||
zassert_string_equal(dst, expect);
|
||||
assert_equal(map.size(), 0);
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - bad1" << std::endl;
|
||||
const std::u32string src = U"6\U0000ffff"; // missing code
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = U"6";
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - bad1b" << std::endl;
|
||||
const std::u32string src = U"6\U0000ffff"; // missing code
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = src;
|
||||
zassert_string_equal(dst, expect);
|
||||
assert_equal(map.size(), 0);
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - bad1c" << std::endl;
|
||||
const std::u32string src = U"6\U0000ffffzz"; // missing code
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = src;
|
||||
zassert_string_equal(dst, expect);
|
||||
assert_equal(map.size(), 0);
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - marker end test" << std::endl;
|
||||
const std::u32string src = U"6\U0000ffff\U00000008\U00000001";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string src = U"6\U0000ffff\U00000008\U00000001";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = U"6";
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{MARKER_BEFORE_EOT, 0x1L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 1);
|
||||
marker_list exp_end = { 0x1L };
|
||||
assert_marker_list_equal(map[MARKER_BEFORE_EOT], exp_end); // marker 1 @ e
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test" << std::endl;
|
||||
const std::u32string src = U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000320\U0000ffff\U00000008\U00000003\U00000300\U0000ffff\U00000008\U00000004";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string src =
|
||||
U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000320\U0000ffff\U00000008\U00000003\U00000300"
|
||||
U"\U0000ffff\U00000008\U00000004";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = U"6e\U00000320\U00000300";
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{U'e', 0x1L}, {0x0320, 0x2L}, {0x0300, 0x3L}, {MARKER_BEFORE_EOT, 0x4L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 4);
|
||||
marker_list exp_e = { 0x1L };
|
||||
assert_marker_list_equal(map[U'e'], exp_e);
|
||||
marker_list exp_320 = { 0x2L };
|
||||
assert_marker_list_equal(map[0x0320], exp_320);
|
||||
marker_list exp_300 = { 0x3L };
|
||||
assert_marker_list_equal(map[0x0300], exp_300);
|
||||
marker_list exp_end = { 0x4L };
|
||||
assert_marker_list_equal(map[MARKER_BEFORE_EOT], exp_end);
|
||||
}
|
||||
{
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - prepend hex quad" << std::endl;
|
||||
|
|
@ -712,17 +733,17 @@ int test_strutils() {
|
|||
assert_equal(parse_hex_quad(U"CAFE"), 0xCAFE);
|
||||
assert_equal(parse_hex_quad(U"D00d"), 0xD00D);
|
||||
assert_equal(parse_hex_quad(U"FFFF"), 0xFFFF);
|
||||
assert_equal(parse_hex_quad(U"zzzz"), 0); // err
|
||||
assert_equal(parse_hex_quad(U"zzzz"), 0); // err
|
||||
}
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
int test_normalize() {
|
||||
int
|
||||
test_normalize() {
|
||||
std::cout << "== " << __FUNCTION__ << std::endl;
|
||||
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " * normalize_nfd_markers" << std::endl;
|
||||
|
||||
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - noop test" << std::endl;
|
||||
|
|
@ -751,38 +772,37 @@ int test_normalize() {
|
|||
U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000320\U0000ffff\U00000008\U00000003\U00000300"
|
||||
U"\U0000ffff\U00000008\U00000004";
|
||||
const std::u32string expect = src;
|
||||
std::u32string dst = src;
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map));
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{U'e', 0x1L}, {0x320, 0x2L}, {0x300, 0x3L}, {MARKER_BEFORE_EOT, 0x4L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 4);
|
||||
assert_marker_list_equal(map[U'e'], marker_list({0x1L}));
|
||||
assert_marker_list_equal(map[0x0320], marker_list({0x2L}));
|
||||
assert_marker_list_equal(map[0x0300], marker_list({0x3L}));
|
||||
assert_marker_list_equal(map[MARKER_BEFORE_EOT],marker_list({0x4L}));
|
||||
}
|
||||
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test" << std::endl;
|
||||
const std::u32string src = // already in order: 320+300
|
||||
U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000320\U0000ffff\U00000008\U00000003\U00000300\U0000ffff\U00000008\U00000004";
|
||||
const std::u32string src = // already in order: 320+300
|
||||
U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000320\U0000ffff\U00000008\U00000003\U00000300"
|
||||
U"\U0000ffff\U00000008\U00000004";
|
||||
const std::u32string expect = src;
|
||||
std::u32string dst = src;
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map));
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{U'e', 0x1L}, {0x320, 0x2L}, {0x300, 0x3L}, {MARKER_BEFORE_EOT, 0x4L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 4);
|
||||
assert_marker_list_equal(map[U'e'], marker_list({0x1L}));
|
||||
assert_marker_list_equal(map[0x0320], marker_list({0x2L}));
|
||||
assert_marker_list_equal(map[0x0300], marker_list({0x3L}));
|
||||
assert_marker_list_equal(map[MARKER_BEFORE_EOT], marker_list({0x4L}));
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test2" << std::endl;
|
||||
const std::u32string src = // out of order, 300-320
|
||||
U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000300\U0000ffff\U00000008\U00000003\U00000320\U0000ffff\U00000008\U00000004";
|
||||
const std::u32string src = // out of order, 300-320
|
||||
U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000002\U00000300\U0000ffff\U00000008\U00000003\U00000320"
|
||||
U"\U0000ffff\U00000008\U00000004";
|
||||
const std::u32string expect =
|
||||
U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000003\U00000320\U0000ffff\U00000008\U00000002\U00000300\U0000ffff\U00000008\U00000004";
|
||||
U"6\U0000ffff\U00000008\U00000001e\U0000ffff\U00000008\U00000003\U00000320\U0000ffff\U00000008\U00000002\U00000300"
|
||||
U"\U0000ffff\U00000008\U00000004";
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map));
|
||||
if (dst != expect) {
|
||||
|
|
@ -790,11 +810,9 @@ int test_normalize() {
|
|||
std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl;
|
||||
}
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{U'e', 0x1L}, {0x300, 0x2L}, {0x320, 0x3L}, {MARKER_BEFORE_EOT, 0x4L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 4);
|
||||
assert_marker_list_equal(map[U'e'], marker_list({0x1L}));
|
||||
assert_marker_list_equal(map[0x0320], marker_list({0x3L}));
|
||||
assert_marker_list_equal(map[0x0300], marker_list({0x2L}));
|
||||
assert_marker_list_equal(map[MARKER_BEFORE_EOT], marker_list({0x4L}));
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -803,15 +821,16 @@ int test_normalize() {
|
|||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test 4a" << std::endl;
|
||||
const std::u32string src = U"4e\u0300\uFFFF\u0008\u0001\u0320";
|
||||
const std::u32string expect = U"4e\uFFFF\u0008\u0001\u0320\u0300";
|
||||
std::u32string dst = src;
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map));
|
||||
if (dst != expect) {
|
||||
std::cout << "dst: " << Debug_UnicodeString(dst) << std::endl;
|
||||
std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl;
|
||||
}
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{0x320, 0x1L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 1);
|
||||
assert_marker_list_equal(map[0x0320], marker_list({0x1L}));
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -820,51 +839,53 @@ int test_normalize() {
|
|||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test 9c" << std::endl;
|
||||
const std::u32string src = U"9ce\u0300\uFFFF\u0008\u0002\u0320\uFFFF\u0008\u0001";
|
||||
const std::u32string expect = U"9ce\uFFFF\u0008\u0002\u0320\u0300\uFFFF\u0008\u0001";
|
||||
std::u32string dst = src;
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map));
|
||||
if (dst != expect) {
|
||||
std::cout << "dst: " << Debug_UnicodeString(dst) << std::endl;
|
||||
std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl;
|
||||
}
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{0x320, 0x2L}, {MARKER_BEFORE_EOT, 0x1L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 2);
|
||||
assert_marker_list_equal(map[0x0320], marker_list({0x2L}));
|
||||
assert_marker_list_equal(map[MARKER_BEFORE_EOT], marker_list({0x1L}));
|
||||
}
|
||||
|
||||
{
|
||||
// from tests - regex edition
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test 9c+regex" << std::endl;
|
||||
const std::u32string src = U"9ce\u0300\uFFFF\u0008\\u0002\u0320\uFFFF\u0008\\u0001";
|
||||
const std::u32string expect = U"9ce\uFFFF\u0008\\u0002\u0320\u0300\uFFFF\u0008\\u0001";
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map, regex_sentinel)); // TODO-LDML: need regex flag
|
||||
const std::u32string src = U"9ce\u0300\\uffff\\u0008\\u0002\u0320\\uffff\\u0008\\u0001";
|
||||
const std::u32string expect = U"9ce\\uffff\\u0008\\u0002\u0320\u0300\\uffff\\u0008\\u0001";
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map, regex_sentinel));
|
||||
if (dst != expect) {
|
||||
std::cout << "dst: " << Debug_UnicodeString(dst) << std::endl;
|
||||
std::cout << " " << dst << std::endl;
|
||||
std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl;
|
||||
std::cout << " " << expect << std::endl;
|
||||
}
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{0x320, 0x2L}, {MARKER_BEFORE_EOT, 0x1L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 2);
|
||||
assert_marker_list_equal(map[0x0320], marker_list({0x2L}));
|
||||
assert_marker_list_equal(map[MARKER_BEFORE_EOT], marker_list({0x1L}));
|
||||
}
|
||||
{
|
||||
// from tests - regex edition
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test \\m{.}" << std::endl;
|
||||
const std::u32string src = U"9ce\u0300\uFFFF\u0008[\\u0001-\\uD7FE]\u0320\uFFFF\u0008\\u0001";
|
||||
const std::u32string expect = U"9ce\uFFFF\u0008[\\u0001-\\uD7FE]\u0320\u0300\uFFFF\u0008\\u0001";
|
||||
std::u32string dst = src;
|
||||
const std::u32string src = U"9ce\u0300\\uffff\\u0008[\\u0001-\\ud7fe]\u0320\\uffff\\u0008\\u0001";
|
||||
const std::u32string expect = U"9ce\\uffff\\u0008[\\u0001-\\ud7fe]\u0320\u0300\\uffff\\u0008\\u0001";
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map, regex_sentinel));
|
||||
if (dst != expect) {
|
||||
std::cout << "dst: " << Debug_UnicodeString(dst) << std::endl;
|
||||
std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl;
|
||||
}
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{0x320, LDML_MARKER_ANY_INDEX}, {MARKER_BEFORE_EOT, 0x1L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 2);
|
||||
assert_marker_list_equal(map[0x0320], marker_list({LDML_MARKER_ANY_INDEX}));
|
||||
assert_marker_list_equal(map[MARKER_BEFORE_EOT], marker_list({0x1L}));
|
||||
}
|
||||
|
||||
{
|
||||
|
|
@ -873,52 +894,72 @@ int test_normalize() {
|
|||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test 10 stack o' 2x2" << std::endl;
|
||||
const std::u32string src = U"9ce\u0300\uFFFF\u0008\u0002\uFFFF\u0008\u0002\u0320";
|
||||
const std::u32string expect = U"9ce\uFFFF\u0008\u0002\uFFFF\u0008\u0002\u0320\u0300";
|
||||
std::u32string dst = src;
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map));
|
||||
if (dst != expect) {
|
||||
std::cout << "dst: " << Debug_UnicodeString(dst) << std::endl;
|
||||
std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl;
|
||||
}
|
||||
assert_equal(map.size(), 1);
|
||||
assert_marker_list_equal(map[0x0320], (marker_list({0x2L, 0x2L})));
|
||||
zassert_string_equal(dst, expect);
|
||||
marker_map expm({{0x320, 0x2L}, {0x320, 0x2L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
assert_equal(map.size(), 2);
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
// from tests
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - complex test 10 stack o' 2x1x2" << std::endl;
|
||||
const std::u32string src = U"9ce\u0300\uFFFF\u0008\u0002\uFFFF\u0008\u0001\uFFFF\u0008\u0003\u0320";
|
||||
const std::u32string expect = U"9ce\uFFFF\u0008\u0002\uFFFF\u0008\u0001\uFFFF\u0008\u0003\u0320\u0300";
|
||||
std::u32string dst = src;
|
||||
std::u32string dst = src;
|
||||
assert(normalize_nfd_markers_segment(dst, map));
|
||||
if (dst != expect) {
|
||||
std::cout << "dst: " << Debug_UnicodeString(dst) << std::endl;
|
||||
std::cout << "exp: " << Debug_UnicodeString(expect) << std::endl;
|
||||
}
|
||||
zassert_string_equal(dst, expect);
|
||||
assert_equal(map.size(), 1);
|
||||
assert_marker_list_equal(map[0x0320], (marker_list({0x2L, 0x1L, 0x3L})));
|
||||
marker_map expm({{0x320, 0x2L}, {0x320, 0x1L}, {0x320, 0x3L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
}
|
||||
|
||||
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - dup-char test" << std::endl;
|
||||
const std::u32string src = U"a\uFFFF\u0008\u0001\u0300e\uFFFF\u0008\u0002\u0300";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = U"a\u0300e\u0300"; // U+0300 twice! This should be removed in 2 segments
|
||||
const std::u32string src = U"a\uFFFF\u0008\u0001\u0300e\uFFFF\u0008\u0002\u0300";
|
||||
const std::u32string dst = remove_markers(src, map);
|
||||
const std::u32string expect = U"a\u0300e\u0300"; // U+0300 twice! This should be removed in 2 segments
|
||||
zassert_string_equal(dst, expect);
|
||||
assert_equal(map.size(), 1);
|
||||
marker_list exp_ae = { 0x1L, 0x2L }; // Not what the user would see in practice.
|
||||
assert_marker_list_equal(map[0x0300], exp_ae); // marker 1 @ e
|
||||
marker_map expm({{0x300, 0x1L}, {0x300, 0x2L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
}
|
||||
{
|
||||
marker_map map;
|
||||
std::cout << __FILE__ << ":" << __LINE__ << " - support 2-segment markers " << std::endl;
|
||||
// e\m{1}`\m{2}_E\m{3}`\m{4}_
|
||||
const std::u32string src =
|
||||
U"e\uFFFF\u0008\u0001\u0300\uFFFF\u0008\u0002\u0320E\uFFFF\u0008\u0003\u0300\uFFFF\u0008\u0004\u0320";
|
||||
// e\m{2}_\m{1}`E\m{4}_\m{3}`
|
||||
const std::u32string expect_rem =
|
||||
U"e\u0300\u0320E\u0300\u0320";
|
||||
const std::u32string expect_nfd =
|
||||
U"e\uFFFF\u0008\u0002\u0320\uFFFF\u0008\u0001\u0300E\uFFFF\u0008\u0004\u0320\uFFFF\u0008\u0003\u0300";
|
||||
auto dst_rem = remove_markers(src, &map); // note: this is bigger than a single segment. so it is a degenerate test case.
|
||||
marker_map expm({{0x300, 0x1L}, {0x320, 0x2L}, {0x300, 0x3L}, {0x320, 0x4L}});
|
||||
assert_marker_map_equal(map, expm);
|
||||
zassert_string_equal(dst_rem, expect_rem);
|
||||
std::u32string dst_nfd = src;
|
||||
assert(normalize_nfd_markers(dst_nfd));
|
||||
if (dst_nfd != expect_nfd) {
|
||||
std::cout << "dst: " << Debug_UnicodeString(dst_nfd) << std::endl;
|
||||
std::cout << "exp: " << Debug_UnicodeString(expect_nfd) << std::endl;
|
||||
}
|
||||
zassert_string_equal(dst_nfd, expect_nfd);
|
||||
}
|
||||
|
||||
return EXIT_SUCCESS;
|
||||
}
|
||||
|
||||
|
||||
int
|
||||
main(int argc, const char *argv[]) {
|
||||
int rc = EXIT_SUCCESS;
|
||||
|
|
@ -929,7 +970,7 @@ main(int argc, const char *argv[]) {
|
|||
|
||||
if (first_arg < argc) {
|
||||
arg_color = std::string(argv[first_arg]) == "--color";
|
||||
if(arg_color) {
|
||||
if (arg_color) {
|
||||
first_arg++;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,6 +36,12 @@ export class StrsCompiler extends EmptyCompiler {
|
|||
const badStringAnalyzer = new util.BadStringAnalyzer();
|
||||
const CONTAINS_MARKER_REGEX = new RegExp(MarkerParser.ANY_MARKER_MATCH);
|
||||
for (let s of strs.allProcessedStrings.values()) {
|
||||
// replace all \\uXXXX with the actual code point.
|
||||
// this lets us analyze whether there are PUA, unassigned, etc.
|
||||
// the results might not be valid regex of course.
|
||||
if (util.CONTAINS_QUAD_ESCAPE.test(s)) {
|
||||
s = util.unescapeQuadString(s);
|
||||
}
|
||||
// skip marker strings
|
||||
if (CONTAINS_MARKER_REGEX.test(s)) {
|
||||
// it had a marker, take out all marker strings, as the sentinel is illegal
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { constants, SectionIdent } from "@keymanapp/ldml-keyboard-constants";
|
||||
import { KMXPlus, LDMLKeyboard, CompilerCallbacks, VariableParser, MarkerParser } from '@keymanapp/common-types';
|
||||
import { KMXPlus, LDMLKeyboard, CompilerCallbacks, VariableParser, MarkerParser, util } from '@keymanapp/common-types';
|
||||
import { SectionCompiler } from "./section-compiler.js";
|
||||
|
||||
import Bksp = KMXPlus.Bksp;
|
||||
|
|
@ -138,9 +138,12 @@ export abstract class TransformCompiler<T extends TransformCompilerType, TranBas
|
|||
// add in markers. idempotent if no markers.
|
||||
cookedFrom = sections.vars.substituteMarkerString(cookedFrom, true);
|
||||
|
||||
// don't unescape literals here, because we are going to pass them through into the regex
|
||||
cookedFrom = util.unescapeStringToRegex(cookedFrom);
|
||||
|
||||
// cookedFrom is cooked above, since there's some special treatment
|
||||
result.from = sections.strs.allocString(cookedFrom, {
|
||||
unescape: true
|
||||
unescape: false,
|
||||
}, sections);
|
||||
// 'to' is handled via allocString
|
||||
result.to = sections.strs.allocString(transform.to, {
|
||||
|
|
|
|||
|
|
@ -154,7 +154,7 @@ block(bksp)
|
|||
00 00 00 00 # KMX_DWORD index = 0
|
||||
|
||||
# transforms 0
|
||||
index(strNull,strElemTranFrom1b,2) # KMX_DWORD str from ^e
|
||||
index(strNull,strElemTranFrom1b,2) # KMX_DWORD str from ^e ## << ??
|
||||
index(strNull,strNull,2) # KMX_DWORD str to 0
|
||||
index(strNull,strNull,2) # KMX_DWORD str mapFrom 0
|
||||
index(strNull,strNull,2) # KMX_DWORD str mapTo 0
|
||||
|
|
@ -493,6 +493,7 @@ block(strs) # struct COMP_KMXPLUS_STRS {
|
|||
diff(strs,strFromSet) sizeof(strFromSet,2)
|
||||
diff(strs,strUSet) sizeof(strUSet,2)
|
||||
diff(strs,strAmarker) sizeof(strAmarker,2)
|
||||
diff(strs,strSentinel0001r) sizeof(strSentinel0001r,2)
|
||||
diff(strs,strElemTranFrom1) sizeof(strElemTranFrom1,2)
|
||||
diff(strs,strElemTranFrom1a) sizeof(strElemTranFrom1a,2)
|
||||
diff(strs,strElemTranFrom1b) sizeof(strElemTranFrom1b,2)
|
||||
|
|
@ -516,7 +517,6 @@ block(strs) # struct COMP_KMXPLUS_STRS {
|
|||
diff(strs,strKeys) sizeof(strKeys,2)
|
||||
diff(strs,strIndicator) sizeof(strIndicator,2)
|
||||
diff(strs,strSentinel0001) sizeof(strSentinel0001,2)
|
||||
diff(strs,strSentinel0001r) sizeof(strSentinel0001r,2)
|
||||
|
||||
# String table -- block(x) is used to store the null u16char at end of each string
|
||||
# without interfering with sizeof() calculation above
|
||||
|
|
@ -528,6 +528,7 @@ block(strs) # struct COMP_KMXPLUS_STRS {
|
|||
block(strFromSet) 5B 00 5C 00 75 00 31 00 41 00 37 00 35 00 2D 00 5C 00 75 00 31 00 41 00 37 00 39 00 5D 00 block(x) 00 00 # [\u1a75-\u1a79]
|
||||
block(strUSet) 5b 00 61 00 62 00 63 00 5d 00 block(x) 00 00 # '[abc]'
|
||||
block(strAmarker) 5C 00 6D 00 7B 00 61 00 7D 00 block(x) 00 00 # '\m{a}'
|
||||
block(strSentinel0001r) 5c 00 75 00 66 00 66 00 66 00 66 00 5c 00 75 00 30 00 30 00 30 00 38 00 5C 00 75 00 30 00 30 00 30 00 31 00 block(x) 00 00 # UC_SENTINEL CODE_DEADKEY \u0001 (regex form)
|
||||
block(strElemTranFrom1) 5E 00 block(x) 00 00 # '^'
|
||||
block(strElemTranFrom1a) 5E 00 61 00 block(x) 00 00 # '^a'
|
||||
block(strElemTranFrom1b) 5E 00 65 00 block(x) 00 00 # '^e'
|
||||
|
|
@ -556,7 +557,6 @@ block(strs) # struct COMP_KMXPLUS_STRS {
|
|||
# <reorder before="ᩫ" from="᩠᩵ᩅ" order="10 55 10" />
|
||||
block(strIndicator) 3d d8 40 de block(x) 00 00 # '🙀'
|
||||
block(strSentinel0001) FF FF 08 00 01 00 block(x) 00 00 # UC_SENTINEL CODE_DEADKEY U+0001
|
||||
block(strSentinel0001r) FF FF 08 00 5C 00 75 00 30 00 30 00 30 00 31 00 block(x) 00 00 # UC_SENTINEL CODE_DEADKEY \u0001 (regex form)
|
||||
|
||||
|
||||
block(endstrs) # end of strs block
|
||||
|
|
|
|||
|
|
@ -48,7 +48,7 @@
|
|||
|
||||
<transformGroup>
|
||||
<!-- Northern Thai example from spec -->
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}[\u1A75-\u1A79]\u{1A45}" order="10 55 10" />
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}[\u{1A75}-\u{1A79}]\u{1A45}" order="10 55 10" />
|
||||
</transformGroup>
|
||||
</transforms>
|
||||
|
||||
|
|
|
|||
|
|
@ -8,12 +8,12 @@
|
|||
<transforms type="simple">
|
||||
<transformGroup>
|
||||
<!-- from the spec: nod-Lana for ᩅᩫ᩶ -->
|
||||
<reorder from="\u1A60" order="127" />
|
||||
<reorder from="\u1A6B" order="42" />
|
||||
<reorder from="[\u1A75-\u1A79]" order="55" />
|
||||
<reorder before="\u1A6B" from="\u1A60\u1A45" order="10" />
|
||||
<reorder before="\u1A6B[\u1A75-\u1A79]" from="\u1A60\u1A45" order="10" />
|
||||
<reorder before="\u1A6B" from="\u1A60[\u1A75-\u1A79]\u1A45" order="10 55 10" />
|
||||
<reorder from="\u{1A60}" order="127" />
|
||||
<reorder from="\u{1A6B}" order="42" />
|
||||
<reorder from="[\u{1A75}-\u{1A79}]" order="55" />
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}\u{1A45}" order="10" />
|
||||
<reorder before="\u{1A6B}[\u{1A75}-\u{1A79}]" from="\u{1A60}\u{1A45}" order="10" />
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}[\u{1A75}-\u{1A79}]\u{1A45}" order="10 55 10" />
|
||||
</transformGroup>
|
||||
</transforms>
|
||||
</keyboard3>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
@@keys: [K_Q][K_W][K_Q]
|
||||
@@expected: \u0127\u1790\u17B6\u0127
|
||||
-->
|
||||
<!DOCTYPE keyboard3 SYSTEM "../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
|
||||
<!DOCTYPE keyboard3 SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
|
||||
<keyboard3 locale="mt" conformsTo="techpreview">
|
||||
<version number="1.0.0" />
|
||||
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
<transformGroup>
|
||||
<!-- Northern Thai example from spec -->
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}[\u1A75-\u1A79]\u{1A45}" order="10 55 10" />
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}[\u{1A75}-\u{1A79}]\u{1A45}" order="10 55 10" />
|
||||
</transformGroup>
|
||||
</transforms>
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
@@keys: [K_Q][K_W][K_Q]
|
||||
@@expected: \u0127\u1790\u17B6\u0127
|
||||
-->
|
||||
<!DOCTYPE keyboard3 SYSTEM "../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
|
||||
<!DOCTYPE keyboard3 SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
|
||||
<keyboard3 locale="mt" conformsTo="techpreview">
|
||||
<version number="1.0.0" />
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
@@keys: [K_Q][K_W][K_Q]
|
||||
@@expected: \u0127\u1790\u17B6\u0127
|
||||
-->
|
||||
<!DOCTYPE keyboard3 SYSTEM "../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
|
||||
<!DOCTYPE keyboard3 SYSTEM "../../../../../../../resources/standards-data/ldml-keyboards/techpreview/dtd/ldmlKeyboard3.dtd">
|
||||
<keyboard3 locale="mt" conformsTo="techpreview">
|
||||
<version number="1.0.0" />
|
||||
|
||||
|
|
@ -46,7 +46,7 @@
|
|||
|
||||
<transformGroup>
|
||||
<!-- Northern Thai example from spec -->
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}[\u1A75-\u1A79]\u{1A45}" order="10 55 10" />
|
||||
<reorder before="\u{1A6B}" from="\u{1A60}[\u{1A75}-\u{1A79}]\u{1A45}" order="10 55 10" />
|
||||
</transformGroup>
|
||||
</transforms>
|
||||
|
||||
|
|
|
|||
|
|
@ -146,10 +146,6 @@ public class TextField: UITextField, KeymanResponder {
|
|||
font = UIFont.systemFont(ofSize: fontSize)
|
||||
}
|
||||
|
||||
if isFirstResponder {
|
||||
resignFirstResponder()
|
||||
becomeFirstResponder()
|
||||
}
|
||||
log.debug("TextField \(self.hashValue) setFont: \(font?.familyName ?? "nil")")
|
||||
}
|
||||
|
||||
|
|
@ -179,7 +175,7 @@ extension KeymanResponder where Self: TextField {
|
|||
resignFirstResponder()
|
||||
Manager.shared.inputViewController.endEditing(true)
|
||||
}
|
||||
|
||||
|
||||
public func summonKeyboard() {
|
||||
becomeFirstResponder()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -133,11 +133,6 @@ public class TextView: UITextView, KeymanResponder {
|
|||
font = UIFont.systemFont(ofSize: fontSize)
|
||||
}
|
||||
|
||||
if isFirstResponder {
|
||||
resignFirstResponder()
|
||||
becomeFirstResponder()
|
||||
}
|
||||
|
||||
log.debug("TextView: \(self.hashValue) setFont: \(font?.familyName ?? "nil")")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -81,6 +81,14 @@ function showBanner(flag) {
|
|||
|
||||
function setBannerImage(path) {
|
||||
bannerImgPath = path;
|
||||
|
||||
var bc = keyman && keyman.osk && keyman.osk.bannerController;
|
||||
if(!bc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// If an inactive banner is set, update its image.
|
||||
bc.inactiveBanner = bc.inactiveBanner ? new bc.ImageBanner(bannerImgPath) : null;
|
||||
}
|
||||
|
||||
function setBannerHeight(h) {
|
||||
|
|
|
|||
|
|
@ -8,13 +8,13 @@ ext.rootPath = '../../../../android'
|
|||
apply from: "$rootPath/version.gradle"
|
||||
|
||||
android {
|
||||
compileSdkVersion 33
|
||||
compileSdk 34
|
||||
namespace="com.firstvoices.keyboards"
|
||||
|
||||
defaultConfig {
|
||||
applicationId "com.firstvoices.keyboards"
|
||||
minSdkVersion 21
|
||||
targetSdkVersion 33
|
||||
targetSdkVersion 34
|
||||
|
||||
// VERSION_CODE and VERSION_NAME from version.gradle
|
||||
versionCode VERSION_CODE as Integer
|
||||
|
|
|
|||
|
|
@ -129,7 +129,7 @@
|
|||
<transforms type="simple">
|
||||
<transformGroup>
|
||||
<transform from="\u{09C7}\m{A}" to="\u{09CC}" /> <!-- E + lengthener = AU -->
|
||||
<!-- <transform from="\u{09C7}\u{09BE}" to="\u09CB" /> --> <!-- E + O = O This is handled by normalization. -->
|
||||
<!-- <transform from="\u{09C7}\u{09BE}" to="\u{09CB}" /> --> <!-- E + O = O This is handled by normalization. -->
|
||||
|
||||
<!-- these suport the 'q' key -->
|
||||
<transform from="\m{q}:" to="\u{0983}" />
|
||||
|
|
|
|||
|
|
@ -8,7 +8,7 @@
|
|||
<info author="Team Keyboard" name="French AZERTY optimisé" layout="AZERTY" indicator="FR" />
|
||||
|
||||
<displays>
|
||||
<display output="\u0300" display="${grave}" />
|
||||
<display output="\u{0300}" display="${grave}" />
|
||||
<display output="\m{acute}" display="´" />
|
||||
<display output="\m{grave}" display="`" />
|
||||
<display output="\m{umlaut}" display="¨" />
|
||||
|
|
@ -191,9 +191,9 @@
|
|||
<transformGroup>
|
||||
<!-- this is a reorder group -->
|
||||
<!-- nod-Lana partial example -->
|
||||
<reorder from="\u1A60" order="127" />
|
||||
<reorder from="\u1A6B" order="42" />
|
||||
<reorder from="[\u1A75-\u1A79]" order="55" />
|
||||
<reorder from="\u{1A60}" order="127" />
|
||||
<reorder from="\u{1A6B}" order="42" />
|
||||
<reorder from="[\u{1A75}-\u{1A79}]" order="55" />
|
||||
<!-- ... partial example ... -->
|
||||
</transformGroup>
|
||||
</transforms>
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ import { createUnselectableElement } from 'keyman/engine/dom-utils';
|
|||
|
||||
export abstract class Banner {
|
||||
private _height: number; // pixels
|
||||
private _width: number; // pixels
|
||||
private div: HTMLDivElement;
|
||||
|
||||
public static DEFAULT_HEIGHT: number = 37; // pixels; embedded apps can modify
|
||||
|
|
@ -47,6 +48,15 @@ export abstract class Banner {
|
|||
this.update();
|
||||
}
|
||||
|
||||
public get width(): number {
|
||||
return this._width;
|
||||
}
|
||||
|
||||
public set width(width: number) {
|
||||
this._width = width;
|
||||
this.update();
|
||||
}
|
||||
|
||||
/**
|
||||
* Function update
|
||||
* @return {boolean} true if the banner styling changed
|
||||
|
|
@ -88,7 +98,7 @@ export abstract class Banner {
|
|||
* Function getDiv
|
||||
* Scope Public
|
||||
* @returns {HTMLElement} Base element of the banner
|
||||
* Description Returns the HTMLElelemnt of the banner
|
||||
* Description Returns the HTMLElement of the banner
|
||||
*/
|
||||
public getDiv(): HTMLElement {
|
||||
return this.div;
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@ import { BannerView } from './bannerView.js';
|
|||
import { Banner } from './banner.js';
|
||||
import { BlankBanner } from './blankBanner.js';
|
||||
import { HTMLBanner } from './htmlBanner.js';
|
||||
import { Keyboard, KeyboardProperties } from '@keymanapp/keyboard-processor';
|
||||
|
||||
export class BannerController {
|
||||
private container: BannerView;
|
||||
|
|
@ -16,6 +17,9 @@ export class BannerController {
|
|||
|
||||
private _inactiveBanner: Banner;
|
||||
|
||||
private keyboard: Keyboard;
|
||||
private keyboardStub: KeyboardProperties;
|
||||
|
||||
/**
|
||||
* Builds a banner for use when predictions are not active, supporting a single image.
|
||||
*/
|
||||
|
|
@ -92,6 +96,23 @@ export class BannerController {
|
|||
selectBanner(state: StateChangeEnum) {
|
||||
// Only display a SuggestionBanner when LanguageProcessor states it is active.
|
||||
this.activateBanner(state == 'active' || state == 'configured');
|
||||
|
||||
if(this.keyboard) {
|
||||
this.container.banner.configureForKeyboard(this.keyboard, this.keyboardStub);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Allows banners to adapt based on the active keyboard and related properties, such as
|
||||
* associated fonts.
|
||||
* @param keyboard
|
||||
* @param keyboardProperties
|
||||
*/
|
||||
public configureForKeyboard(keyboard: Keyboard, keyboardProperties: KeyboardProperties) {
|
||||
this.keyboard = keyboard;
|
||||
this.keyboardStub = keyboardProperties;
|
||||
|
||||
this.container.banner.configureForKeyboard(keyboard, keyboardProperties);
|
||||
}
|
||||
|
||||
public shutdown() {
|
||||
|
|
|
|||
46
web/src/engine/osk/src/banner/bannerScrollState.ts
Normal file
46
web/src/engine/osk/src/banner/bannerScrollState.ts
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
import { InputSample } from "@keymanapp/gesture-recognizer";
|
||||
|
||||
/**
|
||||
* The amount of coordinate 'noise' allowed during a scroll-enabled touch
|
||||
* before interpreting the currently-ongoing touch command as having scrolled.
|
||||
*/
|
||||
const HAS_SCROLLED_FUDGE_FACTOR = 10;
|
||||
|
||||
/**
|
||||
* This class was added to facilitate scroll handling for overflow-x elements, though it could
|
||||
* be extended in the future to accept overflow-y if needed.
|
||||
*
|
||||
* This is necessary because of the OSK's need to use `.preventDefault()` for stability; that
|
||||
* same method blocks native handling of overflow scrolling for touch browsers.
|
||||
*/
|
||||
export class BannerScrollState {
|
||||
totalLength = 0;
|
||||
|
||||
baseCoord: InputSample<any>;
|
||||
curCoord: InputSample<any>;
|
||||
baseScrollLeft: number;
|
||||
|
||||
constructor(coord: InputSample<any>, baseScrollLeft: number) {
|
||||
this.baseCoord = coord;
|
||||
this.curCoord = coord;
|
||||
this.baseScrollLeft = baseScrollLeft;
|
||||
|
||||
this.totalLength = 0;
|
||||
}
|
||||
|
||||
updateTo(coord: InputSample<any>): number {
|
||||
let prevCoord = this.curCoord;
|
||||
this.curCoord = coord;
|
||||
|
||||
let delta = this.baseCoord.targetX - this.curCoord.targetX + this.baseScrollLeft;
|
||||
// Track the total amount of scrolling used, even if just a pixel-wide back and forth wiggle.
|
||||
this.totalLength += Math.abs(this.curCoord.targetX - prevCoord.targetX);
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
public get hasScrolled(): boolean {
|
||||
// Allow an accidental fudge-factor for overflow element noise during a touch, but not much.
|
||||
return this.totalLength > HAS_SCROLLED_FUDGE_FACTOR;
|
||||
}
|
||||
}
|
||||
|
|
@ -24,38 +24,8 @@ interface BannerViewEventMap {
|
|||
}
|
||||
|
||||
/**
|
||||
* The `BannerManager` module is designed to serve as a manager for the
|
||||
* different `Banner` types.
|
||||
* To facilitate this, it will provide a root element property that serves
|
||||
* as a container for any active `Banner`, helping KMW to avoid needless
|
||||
* DOM element shuffling.
|
||||
*
|
||||
* Goals for the `BannerManager`:
|
||||
*
|
||||
* * It will be exposed as `keyman.osk.banner` and will provide the following API:
|
||||
* * `getOptions`, `setOptions` - refer to the `BannerOptions` class for details.
|
||||
* * This provides a persistent point that the web page designers and our
|
||||
* model apps can utilize and can communicate with.
|
||||
* * These API functions are designed for live use and will allow
|
||||
* _hot-swapping_ the `Banner` instance; they're not initialization-only.
|
||||
* * Disabling the `Banner` (even for suggestions) outright with
|
||||
* `enablePredictions == false` will auto-unload any loaded predictive model
|
||||
* from `ModelManager` and setting it to `true` will revert this.
|
||||
* * This should help to avoid wasting computational resources.
|
||||
* * It will listen to ModelManager events and automatically swap Banner
|
||||
* instances as appropriate:
|
||||
* * The option `persistentBanner == true` is designed to replicate current
|
||||
* iOS system keyboard behavior.
|
||||
* * When true, an `ImageBanner` will be displayed.
|
||||
* * If false, it will be replaced with a `BlankBanner` of zero height,
|
||||
* corresponding to our current default lack of banner.
|
||||
* * It will not automatically set `persistentBanner == true`;
|
||||
* this must be set by the iOS app, and only under the following conditions:
|
||||
* * `keyman.isEmbedded == true`
|
||||
* * `device.OS == 'ios'`
|
||||
* * Keyman is being used as the system keyboard within an app that
|
||||
* needs to reserve this space (i.e: Keyman for iOS),
|
||||
* rather than as its standalone app.
|
||||
* The `BannerView` module is designed to serve as the hot-swap container for the
|
||||
* different `Banner` types, helping KMW to avoid needless DOM element shuffling.
|
||||
*/
|
||||
export class BannerView implements OSKViewComponent {
|
||||
private bannerContainer: HTMLDivElement;
|
||||
|
|
@ -161,5 +131,16 @@ export class BannerView implements OSKViewComponent {
|
|||
return ParsedLengthStyle.inPixels(this.height);
|
||||
}
|
||||
|
||||
public refreshLayout() {};
|
||||
public get width(): number | undefined {
|
||||
return this.currentBanner?.width;
|
||||
}
|
||||
|
||||
public set width(w: number) {
|
||||
if(this.currentBanner) {
|
||||
this.currentBanner.width = w;
|
||||
}
|
||||
}
|
||||
|
||||
public refreshLayout() {
|
||||
}
|
||||
}
|
||||
|
|
@ -7,7 +7,8 @@ import {
|
|||
GestureRecognizerConfiguration,
|
||||
GestureSource,
|
||||
InputSample,
|
||||
PaddedZoneSource
|
||||
PaddedZoneSource,
|
||||
RecognitionZoneSource
|
||||
} from '@keymanapp/gesture-recognizer';
|
||||
|
||||
import { BANNER_GESTURE_SET } from './bannerGestureSet.js';
|
||||
|
|
@ -15,12 +16,73 @@ import { BANNER_GESTURE_SET } from './bannerGestureSet.js';
|
|||
import { DeviceSpec, Keyboard, KeyboardProperties } from '@keymanapp/keyboard-processor';
|
||||
import { Banner } from './banner.js';
|
||||
import EventEmitter from 'eventemitter3';
|
||||
import { ParsedLengthStyle } from '../lengthStyle.js';
|
||||
import { getFontSizeStyle } from '../fontSizeUtils.js';
|
||||
import { getTextMetrics } from '../keyboard-layout/getTextMetrics.js';
|
||||
import { BannerScrollState } from './bannerScrollState.js';
|
||||
|
||||
const TOUCHED_CLASS: string = 'kmw-suggest-touched';
|
||||
const BANNER_CLASS: string = 'kmw-suggest-banner';
|
||||
const BANNER_SCROLLER_CLASS = 'kmw-suggest-banner-scroller';
|
||||
|
||||
const BANNER_VERT_ROAMING_HEIGHT_RATIO = 0.666;
|
||||
|
||||
/**
|
||||
* The style to temporarily apply when updating suggestion text in order to prevent
|
||||
* fade transitions at that time.
|
||||
*/
|
||||
const FADE_SWALLOW_STYLE = 'swallow-fade-transition';
|
||||
|
||||
/**
|
||||
* Defines various parameters used by `BannerSuggestion` instances for layout and formatting.
|
||||
* This object is designed first and foremost for use with `BannerSuggestion.update()`.
|
||||
*/
|
||||
interface BannerSuggestionFormatSpec {
|
||||
/**
|
||||
* Sets a minimum width to use for the `BannerSuggestion`'s element; this overrides any
|
||||
* and all settings that would otherwise result in a narrower final width.
|
||||
*/
|
||||
minWidth?: number;
|
||||
|
||||
/**
|
||||
* Sets the width of padding around the text of each suggestion. This should generally match
|
||||
* the 'width' of class = `.kmw-suggest-option::before` and class = `.kmw-suggest-option::after`
|
||||
* elements as defined in kmwosk.css.
|
||||
*/
|
||||
paddingWidth: number,
|
||||
|
||||
/**
|
||||
* The default font size to use for calculations based on relative font-size specs
|
||||
*/
|
||||
emSize: number,
|
||||
|
||||
/**
|
||||
* The font style (font-size, font-family) to use for suggestion-banner display text.
|
||||
*/
|
||||
styleForFont: {
|
||||
fontSize: typeof CSSStyleDeclaration.prototype.fontSize,
|
||||
fontFamily: typeof CSSStyleDeclaration.prototype.fontFamily
|
||||
},
|
||||
|
||||
/**
|
||||
* Sets a target width to use when 'collapsing' suggestions. Only affects those long
|
||||
* enough to need said 'collapsing'.
|
||||
*/
|
||||
collapsedWidth?: number
|
||||
}
|
||||
|
||||
export class BannerSuggestion {
|
||||
div: HTMLDivElement;
|
||||
container: HTMLDivElement;
|
||||
private display: HTMLSpanElement;
|
||||
|
||||
private _collapsedWidth: number;
|
||||
private _textWidth: number;
|
||||
private _minWidth: number;
|
||||
private _paddingWidth: number;
|
||||
|
||||
private fontFamily?: string;
|
||||
private rtl: boolean = false;
|
||||
public readonly rtl: boolean;
|
||||
|
||||
private _suggestion: Suggestion;
|
||||
|
||||
|
|
@ -30,14 +92,19 @@ export class BannerSuggestion {
|
|||
|
||||
constructor(index: number, isRTL: boolean) {
|
||||
this.index = index;
|
||||
this.rtl = isRTL;
|
||||
this.rtl = isRTL ?? false;
|
||||
|
||||
this.constructRoot();
|
||||
|
||||
// Provides an empty, base SPAN for text display. We'll swap these out regularly;
|
||||
// `Suggestion`s will have varying length and may need different styling.
|
||||
let display = this.display = createUnselectableElement('span');
|
||||
this.div.appendChild(display);
|
||||
display.className = 'kmw-suggestion-text';
|
||||
this.container.appendChild(display);
|
||||
}
|
||||
|
||||
get computedStyle() {
|
||||
return getComputedStyle(this.display);
|
||||
}
|
||||
|
||||
private constructRoot() {
|
||||
|
|
@ -46,13 +113,18 @@ export class BannerSuggestion {
|
|||
div.className = "kmw-suggest-option";
|
||||
div.id = BannerSuggestion.BASE_ID + this.index;
|
||||
|
||||
// Ensures that a reasonable width % is set.
|
||||
let usableWidth = 100 - SuggestionBanner.MARGIN * (SuggestionBanner.SUGGESTION_LIMIT - 1);
|
||||
let widthpc = usableWidth / SuggestionBanner.SUGGESTION_LIMIT;
|
||||
|
||||
ds.width = widthpc + '%';
|
||||
|
||||
this.div['suggestion'] = this;
|
||||
|
||||
let container = this.container = document.createElement('div');
|
||||
container.className = "kmw-suggestion-container";
|
||||
|
||||
// Ensures that a reasonable default width, based on % is set. (Since it's not yet in the DOM, we may not yet have actual width info.)
|
||||
let usableWidth = 100 - SuggestionBanner.MARGIN * (SuggestionBanner.LONG_SUGGESTION_DISPLAY_LIMIT - 1);
|
||||
|
||||
let widthpc = usableWidth / (SuggestionBanner.LONG_SUGGESTION_DISPLAY_LIMIT);
|
||||
container.style.minWidth = widthpc + '%';
|
||||
|
||||
div.appendChild(container);
|
||||
}
|
||||
|
||||
public matchKeyboardProperties(keyboardProperties: KeyboardProperties) {
|
||||
|
|
@ -77,30 +149,188 @@ export class BannerSuggestion {
|
|||
|
||||
/**
|
||||
* Function update
|
||||
* @param {string} id Element ID for the suggestion span
|
||||
* @param {Suggestion} suggestion Suggestion from the lexical model
|
||||
* Description Update the ID and text of the BannerSuggestionSpec
|
||||
* @param {BannerSuggestionFormatSpec} format Formatting metadata to use for the Suggestion
|
||||
*
|
||||
* Update the ID and text of the BannerSuggestionSpec
|
||||
*/
|
||||
public update(suggestion: Suggestion) {
|
||||
public update(suggestion: Suggestion, format: BannerSuggestionFormatSpec) {
|
||||
this._suggestion = suggestion;
|
||||
this.updateText();
|
||||
|
||||
let display = this.generateSuggestionText(this.rtl);
|
||||
this.container.replaceChild(display, this.display);
|
||||
this.display = display;
|
||||
|
||||
// Set internal properties for use in format calculations.
|
||||
if(format.minWidth !== undefined) {
|
||||
this._minWidth = format.minWidth;
|
||||
}
|
||||
|
||||
this._paddingWidth = format.paddingWidth;
|
||||
this._collapsedWidth = format.collapsedWidth;
|
||||
|
||||
if(suggestion && suggestion.displayAs) {
|
||||
const rawMetrics = getTextMetrics(suggestion.displayAs, format.emSize, format.styleForFont);
|
||||
this._textWidth = rawMetrics.width;
|
||||
} else {
|
||||
this._textWidth = 0;
|
||||
}
|
||||
|
||||
this.currentWidth = this.collapsedWidth;
|
||||
this.updateLayout();
|
||||
}
|
||||
|
||||
private updateText() {
|
||||
let display = this.generateSuggestionText(this.rtl);
|
||||
this.div.replaceChild(display, this.display);
|
||||
this.display = display;
|
||||
public updateLayout() {
|
||||
if(!this.suggestion && this.index != 0) {
|
||||
this.div.style.width='0px';
|
||||
return;
|
||||
} else {
|
||||
this.div.style.width='';
|
||||
}
|
||||
|
||||
const collapserStyle = this.container.style;
|
||||
collapserStyle.minWidth = this.collapsedWidth + 'px';
|
||||
|
||||
if(this.rtl) {
|
||||
collapserStyle.marginRight = (this.collapsedWidth - this.expandedWidth) + 'px';
|
||||
} else {
|
||||
collapserStyle.marginLeft = (this.collapsedWidth - this.expandedWidth) + 'px';
|
||||
}
|
||||
|
||||
this.updateFade();
|
||||
}
|
||||
|
||||
public updateFade() {
|
||||
// Note: selected suggestion fade transitions are handled purely by CSS.
|
||||
// We want to prevent them when updating a suggestion, though.
|
||||
this.div.classList.add(FADE_SWALLOW_STYLE);
|
||||
// Be sure that our fade-swallow mechanism is able to trigger once;
|
||||
// we'll remove it after the current animation frame.
|
||||
window.requestAnimationFrame(() => {
|
||||
this.div.classList.remove(FADE_SWALLOW_STYLE);
|
||||
})
|
||||
|
||||
// Never apply fading to the side that doesn't overflow.
|
||||
this.div.classList.add(`kmw-hide-fade-${this.rtl ? 'left' : 'right'}`);
|
||||
|
||||
// Matches the side that overflows, depending on if LTR or RTL.
|
||||
const fadeClass = `kmw-hide-fade-${this.rtl ? 'right' : 'left'}`;
|
||||
|
||||
// Is the suggestion already its ideal width?.
|
||||
if(!(this.expandedWidth - this.collapsedWidth)) {
|
||||
// Yes? Don't do any fading.
|
||||
this.div.classList.add(fadeClass);
|
||||
} else {
|
||||
this.div.classList.remove(fadeClass);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Denotes the threshold at which the banner suggestion will no longer gain width
|
||||
* in its default form, resulting in two separate states: "collapsed" and "expanded".
|
||||
*/
|
||||
public get targetCollapsedWidth(): number {
|
||||
return this._collapsedWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The raw width needed to display the suggestion's display text without triggering overflow.
|
||||
*/
|
||||
public get textWidth(): number {
|
||||
return this._textWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* Width of the padding to apply equally on both sides of the suggestion's display text.
|
||||
* Is the sum of both, rather than the value applied to each side.
|
||||
*/
|
||||
public get paddingWidth(): number {
|
||||
return this._paddingWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The absolute minimum width to allow for the represented suggestion's banner element.
|
||||
*/
|
||||
public get minWidth(): number {
|
||||
return this._minWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The absolute minimum width to allow for the represented suggestion's banner element.
|
||||
*/
|
||||
public set minWidth(val: number) {
|
||||
this._minWidth = val;
|
||||
}
|
||||
|
||||
/**
|
||||
* The total width taken by the suggestion's banner element when fully expanded.
|
||||
* This may equal the `collapsed` width for sufficiently short suggestions.
|
||||
*/
|
||||
public get expandedWidth(): number {
|
||||
// minWidth must be defined AND greater for the conditional to return this.minWidth.
|
||||
return this.minWidth > this.spanWidth ? this.minWidth : this.spanWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The total width used by the internal contents of the suggestion's banner element when not obscured.
|
||||
*/
|
||||
public get spanWidth(): number {
|
||||
let spanWidth = this.textWidth ?? 0;
|
||||
if(spanWidth) {
|
||||
spanWidth += this.paddingWidth ?? 0;
|
||||
}
|
||||
|
||||
return spanWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual width to be used for the `BannerSuggestion`'s display element when in the 'collapsed'
|
||||
* state and not transitioning.
|
||||
*/
|
||||
public get collapsedWidth(): number {
|
||||
// Allow shrinking a suggestion's width if it has excess whitespace.
|
||||
let utilizedWidth = this.spanWidth < this.targetCollapsedWidth ? this.spanWidth : this.targetCollapsedWidth;
|
||||
// If a minimum width has been specified, enforce that minimum.
|
||||
let maxWidth = utilizedWidth < this.expandedWidth ? utilizedWidth : this.expandedWidth;
|
||||
|
||||
// Will return maxWidth if this.minWidth is undefined.
|
||||
return (this.minWidth > maxWidth ? this.minWidth : maxWidth);
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual width currently utilized by the `BannerSuggestion`'s display element, regardless of
|
||||
* current state.
|
||||
*/
|
||||
public get currentWidth(): number {
|
||||
return this.div.offsetWidth;
|
||||
}
|
||||
|
||||
/**
|
||||
* The actual width currently utilized by the `BannerSuggestion`'s display element, regardless of
|
||||
* current state.
|
||||
*/
|
||||
public set currentWidth(val: number) {
|
||||
// TODO: probably should set up errors or something here...
|
||||
if(val < this.collapsedWidth) {
|
||||
val = this.collapsedWidth;
|
||||
} else if(val > this.expandedWidth) {
|
||||
val = this.expandedWidth;
|
||||
}
|
||||
|
||||
if(this.rtl) {
|
||||
this.container.style.marginRight = `${val - this.expandedWidth}px`;
|
||||
} else {
|
||||
this.container.style.marginLeft = `${val - this.expandedWidth}px`;
|
||||
}
|
||||
}
|
||||
|
||||
public highlight(on: boolean) {
|
||||
const elem = this.div;
|
||||
let classes = elem.className;
|
||||
let cs = ' ' + SuggestionBanner.TOUCHED_CLASS;
|
||||
|
||||
if(on && classes.indexOf(cs) < 0) {
|
||||
elem.className=classes+cs;
|
||||
if(on) {
|
||||
elem.classList.add(TOUCHED_CLASS);
|
||||
} else {
|
||||
elem.className=classes.replace(cs,'');
|
||||
elem.classList.remove(TOUCHED_CLASS);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -148,7 +378,8 @@ export class BannerSuggestion {
|
|||
* Description Display lexical model suggestions in the banner
|
||||
*/
|
||||
export class SuggestionBanner extends Banner {
|
||||
public static readonly SUGGESTION_LIMIT: number = 3;
|
||||
public static readonly SUGGESTION_LIMIT: number = 8;
|
||||
public static readonly LONG_SUGGESTION_DISPLAY_LIMIT: number = 3;
|
||||
public static readonly MARGIN = 1;
|
||||
|
||||
public readonly type = "suggestion";
|
||||
|
|
@ -158,20 +389,33 @@ export class SuggestionBanner extends Banner {
|
|||
private currentSuggestions: Suggestion[] = [];
|
||||
|
||||
private options : BannerSuggestion[] = [];
|
||||
private separators: HTMLElement[] = [];
|
||||
|
||||
private isRTL: boolean = false;
|
||||
|
||||
private hostDevice: DeviceSpec;
|
||||
|
||||
/**
|
||||
* The banner 'container', which is also the root element for banner scrolling.
|
||||
*/
|
||||
private readonly container: HTMLElement;
|
||||
private highlightAnimation: SuggestionExpandContractAnimation;
|
||||
|
||||
private gestureEngine: GestureRecognizer<BannerSuggestion>;
|
||||
private scrollState: BannerScrollState;
|
||||
private selectionBounds: RecognitionZoneSource;
|
||||
|
||||
private _predictionContext: PredictionContext;
|
||||
|
||||
static readonly TOUCHED_CLASS: string = 'kmw-suggest-touched';
|
||||
static readonly BANNER_CLASS: string = 'kmw-suggest-banner';
|
||||
|
||||
constructor(hostDevice: DeviceSpec, height?: number) {
|
||||
super(height || Banner.DEFAULT_HEIGHT);
|
||||
this.hostDevice = hostDevice;
|
||||
|
||||
this.getDiv().className = this.getDiv().className + ' ' + SuggestionBanner.BANNER_CLASS;
|
||||
|
||||
this.container = document.createElement('div');
|
||||
this.container.className = BANNER_SCROLLER_CLASS;
|
||||
this.getDiv().appendChild(this.container);
|
||||
this.buildInternals(false);
|
||||
|
||||
this.events = new EventEmitter<SuggestionInputEventMap>(); //this.manager.events;
|
||||
|
|
@ -180,9 +424,12 @@ export class SuggestionBanner extends Banner {
|
|||
}
|
||||
|
||||
buildInternals(rtl: boolean) {
|
||||
this.isRTL = rtl;
|
||||
if(this.options.length > 0) {
|
||||
this.options.splice(0, this.options.length); // Clear the array.
|
||||
this.options = [];
|
||||
this.separators = [];
|
||||
}
|
||||
|
||||
for (var i=0; i<SuggestionBanner.SUGGESTION_LIMIT; i++) {
|
||||
let d = new BannerSuggestion(i, rtl);
|
||||
this.options[i] = d;
|
||||
|
|
@ -196,44 +443,58 @@ export class SuggestionBanner extends Banner {
|
|||
* for visuals/UI while still being internally LTR.
|
||||
*/
|
||||
for (var i=0; i<SuggestionBanner.SUGGESTION_LIMIT; i++) {
|
||||
let indexToInsert = rtl ? SuggestionBanner.SUGGESTION_LIMIT - i -1 : i;
|
||||
this.getDiv().appendChild(this.options[indexToInsert].div);
|
||||
let indexToInsert = rtl ? SuggestionBanner.SUGGESTION_LIMIT - i - 1 : i;
|
||||
this.container.appendChild(this.options[indexToInsert].div);
|
||||
|
||||
if(i != SuggestionBanner.SUGGESTION_LIMIT) {
|
||||
// RTL should start right-aligned, thus @ max scroll.
|
||||
if(rtl) {
|
||||
this.container.scrollLeft = this.container.scrollWidth;
|
||||
}
|
||||
|
||||
if(i != SuggestionBanner.SUGGESTION_LIMIT - 1) {
|
||||
// Adds a 'separator' div element for UI purposes.
|
||||
let separatorDiv = createUnselectableElement('div');
|
||||
separatorDiv.className = 'kmw-banner-separator';
|
||||
|
||||
let ds = separatorDiv.style;
|
||||
ds.marginLeft = (SuggestionBanner.MARGIN / 2) + '%';
|
||||
ds.marginRight = (SuggestionBanner.MARGIN / 2) + '%';
|
||||
ds.marginLeft = `calc(${(SuggestionBanner.MARGIN / 2)}% - 0.5px)`;
|
||||
ds.marginRight = `calc(${(SuggestionBanner.MARGIN / 2)}% - 0.5px)`;
|
||||
|
||||
this.getDiv().appendChild(separatorDiv);
|
||||
this.container.appendChild(separatorDiv);
|
||||
// Ensure the separators are maintained in the same order as the
|
||||
// suggestion elements!
|
||||
this.separators[indexToInsert - (rtl ? 1 : 0)] = separatorDiv;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private setupInputHandling(): GestureRecognizer<BannerSuggestion> {
|
||||
|
||||
const findTargetFrom = (e: HTMLElement): HTMLDivElement => {
|
||||
try {
|
||||
if(e) {
|
||||
if(e.classList.contains('kmw-suggest-option')) {
|
||||
return e as HTMLDivElement;
|
||||
}
|
||||
if(e.parentElement && e.parentElement.classList.contains('kmw-suggest-option')) {
|
||||
return e.parentElement as HTMLDivElement;
|
||||
}
|
||||
}
|
||||
} catch(ex) {}
|
||||
return null;
|
||||
}
|
||||
// Auto-cancels suggestion-selection if the finger moves too far; having very generous
|
||||
// safe-zone settings also helps keep scrolls active on demo pages, etc.
|
||||
const safeBounds = new PaddedZoneSource(this.getDiv(), [-Number.MAX_SAFE_INTEGER]);
|
||||
this.selectionBounds = new PaddedZoneSource(
|
||||
this.getDiv(),
|
||||
[-BANNER_VERT_ROAMING_HEIGHT_RATIO * this.height, -Number.MAX_SAFE_INTEGER]
|
||||
);
|
||||
|
||||
const config: GestureRecognizerConfiguration<BannerSuggestion> = {
|
||||
targetRoot: this.getDiv(),
|
||||
maxRoamingBounds: new PaddedZoneSource(this.getDiv(), [-0.333 * this.height]),
|
||||
maxRoamingBounds: safeBounds,
|
||||
safeBounds: safeBounds,
|
||||
// touchEventRoot: this.element, // is the default
|
||||
itemIdentifier: (sample, target: HTMLElement) => {
|
||||
const selBounds = this.selectionBounds.getBoundingClientRect();
|
||||
|
||||
// Step 1: is the coordinate within the range we permit for selecting _anything_?
|
||||
if(sample.clientX < selBounds.left || sample.clientX > selBounds.right) {
|
||||
return null;
|
||||
}
|
||||
if(sample.clientY < selBounds.top || sample.clientY > selBounds.bottom) {
|
||||
return null;
|
||||
}
|
||||
|
||||
// Step 2: find the best-matching selection.
|
||||
|
||||
let bestMatch: BannerSuggestion = null;
|
||||
let bestDist = Number.MAX_VALUE;
|
||||
|
||||
|
|
@ -260,14 +521,33 @@ export class SuggestionBanner extends Banner {
|
|||
|
||||
const sourceTracker: {
|
||||
source: GestureSource<BannerSuggestion>,
|
||||
roamingHighlightHandler: (sample: InputSample<BannerSuggestion>) => void,
|
||||
scrollingHandler: (sample: InputSample<BannerSuggestion>) => void,
|
||||
suggestion: BannerSuggestion
|
||||
} = {
|
||||
source: null,
|
||||
roamingHighlightHandler: null,
|
||||
scrollingHandler: null,
|
||||
suggestion: null
|
||||
};
|
||||
|
||||
const markSelection = (suggestion: BannerSuggestion) => {
|
||||
suggestion.highlight(true);
|
||||
if(this.highlightAnimation) {
|
||||
this.highlightAnimation.cancel();
|
||||
this.highlightAnimation.decouple();
|
||||
}
|
||||
|
||||
this.highlightAnimation = new SuggestionExpandContractAnimation(this.container, suggestion, false);
|
||||
this.highlightAnimation.expand();
|
||||
}
|
||||
|
||||
const clearSelection = (suggestion: BannerSuggestion) => {
|
||||
suggestion.highlight(false);
|
||||
if(!this.highlightAnimation) {
|
||||
this.highlightAnimation = new SuggestionExpandContractAnimation(this.container, suggestion, false);
|
||||
}
|
||||
this.highlightAnimation.collapse();
|
||||
}
|
||||
|
||||
engine.on('inputstart', (source) => {
|
||||
// The banner does not support multi-touch - if one is still current, block all others.
|
||||
if(sourceTracker.source) {
|
||||
|
|
@ -275,41 +555,64 @@ export class SuggestionBanner extends Banner {
|
|||
return;
|
||||
}
|
||||
|
||||
this.scrollState = new BannerScrollState(source.currentSample, this.container.scrollLeft);
|
||||
const suggestion = source.baseItem;
|
||||
|
||||
sourceTracker.source = source;
|
||||
sourceTracker.roamingHighlightHandler = (sample) => {
|
||||
// Maintain highlighting
|
||||
const suggestion = sample.item;
|
||||
sourceTracker.scrollingHandler = (sample) => {
|
||||
const newScrollLeft = this.scrollState.updateTo(sample);
|
||||
this.highlightAnimation.setBaseScroll(newScrollLeft);
|
||||
|
||||
if(suggestion != sourceTracker.suggestion) {
|
||||
sourceTracker.suggestion.highlight(false);
|
||||
suggestion.highlight(true);
|
||||
sourceTracker.suggestion = suggestion;
|
||||
// Only re-enable the original suggestion, even if the touchpoint finds
|
||||
// itself over a different suggestion. Might happen if a scroll boundary
|
||||
// is reached.
|
||||
const incoming = sample.item ? suggestion : null;
|
||||
|
||||
// It's possible to cancel selection while still scrolling.
|
||||
if(incoming != sourceTracker.suggestion) {
|
||||
if(sourceTracker.suggestion) {
|
||||
clearSelection(sourceTracker.suggestion);
|
||||
}
|
||||
};
|
||||
sourceTracker.suggestion = source.currentSample.item;
|
||||
|
||||
sourceTracker.suggestion = incoming;
|
||||
if(incoming) {
|
||||
markSelection(incoming);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
sourceTracker.suggestion = source.currentSample.item;
|
||||
markSelection(sourceTracker.suggestion);
|
||||
|
||||
source.currentSample.item.highlight(true);
|
||||
|
||||
const terminationHandler = () => {
|
||||
sourceTracker.suggestion.highlight(false);
|
||||
if(sourceTracker.suggestion) {
|
||||
clearSelection(sourceTracker.suggestion);
|
||||
sourceTracker.suggestion = null;
|
||||
}
|
||||
|
||||
sourceTracker.source = null;
|
||||
sourceTracker.roamingHighlightHandler = null;
|
||||
sourceTracker.suggestion = null;
|
||||
sourceTracker.scrollingHandler = null;
|
||||
}
|
||||
|
||||
source.path.on('complete', terminationHandler);
|
||||
source.path.on('invalidated', terminationHandler);
|
||||
source.path.on('step', sourceTracker.roamingHighlightHandler);
|
||||
source.path.on('step', sourceTracker.scrollingHandler);
|
||||
});
|
||||
|
||||
engine.on('recognizedgesture', (sequence) => {
|
||||
// The actual result comes in via the sequence's `stage` event.
|
||||
sequence.once('stage', (result) => {
|
||||
const suggestion = result.item; // Should also == sourceTracker.suggestion.
|
||||
if(suggestion) {
|
||||
this.predictionContext.accept(suggestion.suggestion);
|
||||
if(suggestion && !this.scrollState.hasScrolled) {
|
||||
this.predictionContext.accept(suggestion.suggestion).then(() => {
|
||||
// Reset the scroll state
|
||||
this.container.scrollLeft = this.isRTL ? this.container.scrollWidth : 0;
|
||||
});
|
||||
}
|
||||
|
||||
this.scrollState = null;
|
||||
});
|
||||
});
|
||||
|
||||
|
|
@ -322,7 +625,9 @@ export class SuggestionBanner extends Banner {
|
|||
// Ensure the banner's extended recognition zone is based on proper, up-to-date layout info.
|
||||
// Note: during banner init, `this.gestureEngine` may only be defined after
|
||||
// the first call to this setter!
|
||||
(this.gestureEngine?.config.maxRoamingBounds as PaddedZoneSource)?.updatePadding([-0.333 * this.height]);
|
||||
(this.selectionBounds as PaddedZoneSource)?.updatePadding(
|
||||
[-BANNER_VERT_ROAMING_HEIGHT_RATIO * this.height, -Number.MAX_SAFE_INTEGER]
|
||||
);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
|
@ -335,7 +640,7 @@ export class SuggestionBanner extends Banner {
|
|||
// parse incoming HTML.
|
||||
//
|
||||
// Just in case, alternative approaches: https://stackoverflow.com/a/3955238
|
||||
this.getDiv().textContent = '';
|
||||
this.container.textContent = '';
|
||||
|
||||
// Builds new children to match needed RTL properties.
|
||||
this.buildInternals(rtl);
|
||||
|
|
@ -362,16 +667,106 @@ export class SuggestionBanner extends Banner {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Produces a closure useful for updating the SuggestionBanner's UI to match newly-received
|
||||
* suggestions, including optimization of the banner's layout.
|
||||
* @param suggestions
|
||||
*/
|
||||
public onSuggestionUpdate = (suggestions: Suggestion[]): void => {
|
||||
this.currentSuggestions = suggestions;
|
||||
// Immediately stop all animations and reset options accordingly.
|
||||
this.highlightAnimation?.cancel();
|
||||
|
||||
this.options.forEach((option: BannerSuggestion, i: number) => {
|
||||
if(i < suggestions.length) {
|
||||
option.update(suggestions[i]);
|
||||
const fontStyleBase = this.options[0].computedStyle;
|
||||
// Do NOT just re-use the returned object from the line above; it may spontaneously change
|
||||
// (in a bad way) when the underlying span is replaced!
|
||||
const fontStyle = {
|
||||
fontSize: fontStyleBase.fontSize,
|
||||
fontFamily: fontStyleBase.fontFamily
|
||||
}
|
||||
const emSizeStr = getComputedStyle(document.body).fontSize;
|
||||
const emSize = getFontSizeStyle(emSizeStr).val;
|
||||
|
||||
const textStyle = getComputedStyle(this.options[0].container.firstChild as HTMLSpanElement);
|
||||
|
||||
const targetWidth = this.width / SuggestionBanner.LONG_SUGGESTION_DISPLAY_LIMIT;
|
||||
|
||||
// computedStyle will fail if the element's not in the DOM yet.
|
||||
// Seeks to get the values specified within kmwosk.css.
|
||||
const textLeftPad = new ParsedLengthStyle(textStyle.paddingLeft || '4px');
|
||||
const textRightPad = new ParsedLengthStyle(textStyle.paddingRight || '4px');
|
||||
|
||||
let optionFormat: BannerSuggestionFormatSpec = {
|
||||
paddingWidth: textLeftPad.val + textRightPad.val, // Assumes fixed px padding.
|
||||
emSize: emSize,
|
||||
styleForFont: fontStyle,
|
||||
collapsedWidth: targetWidth,
|
||||
minWidth: 0,
|
||||
}
|
||||
|
||||
let totalWidth = 0;
|
||||
let displayCount = 0;
|
||||
|
||||
let collapsedOptions: BannerSuggestion[] = [];
|
||||
|
||||
for (let i=0; i<SuggestionBanner.SUGGESTION_LIMIT; i++) {
|
||||
const d = this.options[i];
|
||||
|
||||
if(suggestions.length > i) {
|
||||
const suggestion = suggestions[i];
|
||||
d.update(suggestion, optionFormat);
|
||||
if(d.collapsedWidth < d.expandedWidth) {
|
||||
collapsedOptions.push(d);
|
||||
}
|
||||
|
||||
totalWidth += d.collapsedWidth;
|
||||
displayCount++;
|
||||
} else {
|
||||
option.update(null);
|
||||
d.update(null, optionFormat);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// Ensure one suggestion is always displayed, even if empty. (Keep the separators out)
|
||||
displayCount = displayCount || 1;
|
||||
|
||||
if(totalWidth < this.width) {
|
||||
let separatorWidth = (this.width * 0.01 * (displayCount-1));
|
||||
// Prioritize adding padding to suggestions that actually need it.
|
||||
// Use equal measure for each so long as it still could use extra display space.
|
||||
while(totalWidth < this.width && collapsedOptions.length > 0) {
|
||||
let maxFillPadding = (this.width - totalWidth - separatorWidth) / collapsedOptions.length;
|
||||
collapsedOptions.sort((a, b) => a.expandedWidth - b.expandedWidth);
|
||||
|
||||
let shortestCollapsed = collapsedOptions[0];
|
||||
let neededWidth = shortestCollapsed.expandedWidth - shortestCollapsed.collapsedWidth;
|
||||
|
||||
let padding = Math.min(neededWidth, maxFillPadding);
|
||||
|
||||
// Check: it is possible that two elements were matched for equal length, thus the second loop's takes no additional padding.
|
||||
// No need to trigger re-layout ops for that case.
|
||||
if(padding > 0) {
|
||||
collapsedOptions.forEach((a) => a.minWidth = a.collapsedWidth + padding);
|
||||
totalWidth += padding * collapsedOptions.length; // don't forget to record that we added the padding!
|
||||
}
|
||||
|
||||
collapsedOptions.splice(0, 1); // discard the element we based our judgment upon; we need not consider it any longer.
|
||||
}
|
||||
|
||||
// If there's STILL leftover padding to distribute, let's do that now.
|
||||
let fillPadding = (this.width - totalWidth - separatorWidth) / displayCount;
|
||||
|
||||
for(let i=0; i < displayCount; i++) {
|
||||
const d = this.options[i];
|
||||
|
||||
d.minWidth = d.collapsedWidth + fillPadding;
|
||||
d.updateLayout();
|
||||
}
|
||||
}
|
||||
|
||||
// Hide any separators beyond the final displayed suggestion
|
||||
for(let i=0; i < SuggestionBanner.SUGGESTION_LIMIT - 1; i++) {
|
||||
this.separators[i].style.display = i < displayCount - 1 ? '' : 'none';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -379,4 +774,260 @@ interface SuggestionInputEventMap {
|
|||
highlight: (bannerSuggestion: BannerSuggestion, state: boolean) => void,
|
||||
apply: (bannerSuggestion: BannerSuggestion) => void;
|
||||
hold: (bannerSuggestion: BannerSuggestion) => void;
|
||||
}
|
||||
scrollLeft: (val: number) => void;
|
||||
}
|
||||
|
||||
|
||||
class SuggestionExpandContractAnimation {
|
||||
private scrollContainer: HTMLElement | null;
|
||||
private option: BannerSuggestion;
|
||||
|
||||
private collapsedScrollOffset: number;
|
||||
private rootScrollOffset: number;
|
||||
|
||||
private startTimestamp: number;
|
||||
private pendingAnimation: number;
|
||||
|
||||
private static TRANSITION_TIME = 250; // in ms.
|
||||
|
||||
constructor(scrollContainer: HTMLElement, option: BannerSuggestion, forRTL: boolean) {
|
||||
this.scrollContainer = scrollContainer;
|
||||
this.option = option;
|
||||
this.collapsedScrollOffset = scrollContainer.scrollLeft;
|
||||
this.rootScrollOffset = scrollContainer.scrollLeft;
|
||||
}
|
||||
|
||||
public setBaseScroll(val: number) {
|
||||
this.collapsedScrollOffset = val;
|
||||
|
||||
// If the user has shifted the scroll position to make more of the element visible, we can remove part
|
||||
// of the corresponding scrolling offset permanently; the user's taken action to view that area.
|
||||
if(this.option.rtl) {
|
||||
// A higher scrollLeft (scrolling right) will reveal more of an initially-clipped suggestion.
|
||||
if(val > this.rootScrollOffset) {
|
||||
this.rootScrollOffset = val;
|
||||
}
|
||||
} else {
|
||||
// Here, a lower scrollLeft (scrolling left).
|
||||
if(val < this.rootScrollOffset) {
|
||||
this.rootScrollOffset = val;
|
||||
}
|
||||
}
|
||||
|
||||
// Synchronize the banner-scroller's offset update with that of the
|
||||
// animation for expansion and collapsing.
|
||||
window.requestAnimationFrame(this.setScrollOffset);
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs mapping of the user's touchpoint to properly-offset scroll coordinates based on
|
||||
* the state of the ongoing scroll operation.
|
||||
*
|
||||
* First priority: this function aims to keep all currently-visible parts of a selected
|
||||
* suggestion visible when first selected. Any currently-clipped parts will remain clipped.
|
||||
*
|
||||
* Second priority: all animations should be smooth and continuous; aesthetics do matter to
|
||||
* users.
|
||||
*
|
||||
* Third priority: when possible without violating the first two priorities, this (in tandem with
|
||||
* adjustments within `setBaseScroll`) will aim to sync the touchpoint with its original
|
||||
* location on an expanded suggestion.
|
||||
* - For LTR languages, this means that suggestions will "expand left" if possible.
|
||||
* - While for RTL languages, they will "expand right" if possible.
|
||||
* - However, if they would expand outside of the banner's effective viewport, a scroll offset
|
||||
* will kick in to enforce the "first priority" mentioned above.
|
||||
* - This "scroll offset" will be progressively removed (because second priority) if and as
|
||||
* the user manually scrolls to reveal relevant space that was originally outside of the viewport.
|
||||
*
|
||||
* @returns
|
||||
*/
|
||||
private setScrollOffset = () => {
|
||||
// If we've been 'decoupled', a different instance (likely for a different suggestion)
|
||||
// is responsible for counter-scrolling.
|
||||
if(!this.scrollContainer) {
|
||||
return;
|
||||
}
|
||||
|
||||
// -- Clamping / "scroll offset" logic --
|
||||
|
||||
// As currently written / defined below, and used internally within this function, "clamping"
|
||||
// refers to alterations to scroll-positioned mapping designed to keep as much of the expanded
|
||||
// option visible as possible via the offsets below (that is, "clamped" to the relevant border)
|
||||
// while not adding extra discontinuity by pushing already-obscured parts of the expanded option
|
||||
// into visible range.
|
||||
//
|
||||
// In essence, it's an extra "scroll offset" we apply that is dynamically adjusted depending on
|
||||
// scroll position as it changes. This offset may be decreased when it is no longer needed to
|
||||
// make parts of the element visible.
|
||||
|
||||
// The amount of extra space being taken by a partially or completely expanded suggestion.
|
||||
const maxWidthToCounterscroll = this.option.currentWidth - this.option.collapsedWidth;
|
||||
const rtl = this.option.rtl;
|
||||
|
||||
// If non-zero, indicates the pixel-width of the collapsed form of the suggestion clipped by the relevant screen border.
|
||||
const ltrOverflow = Math.max(this.rootScrollOffset - this.option.div.offsetLeft, 0);
|
||||
const rtlOverflow = Math.max(this.option.div.offsetLeft + this.option.collapsedWidth - (this.rootScrollOffset + this.scrollContainer.offsetWidth));
|
||||
|
||||
const srcCounterscrollOverflow = Math.max(rtl ? rtlOverflow : ltrOverflow, 0); // positive offset into overflow-land.
|
||||
|
||||
// Base position for scrollLeft clamped within std element scroll bounds, including:
|
||||
// - an adjustment to cover the extra width from expansion
|
||||
// - preserving the base expected overflow levels
|
||||
// Does NOT make adjustments to force extra visibility on the element being highlighted/focused.
|
||||
const unclampedExpandingScrollOffset = Math.max(this.collapsedScrollOffset + (rtl ? 0 : 1) * maxWidthToCounterscroll, 0) + (rtl ? 0 : -1) * srcCounterscrollOverflow;
|
||||
// The same, but for our 'root scroll coordinate'.
|
||||
const rootUnclampedExpandingScrollOffset = Math.max(this.rootScrollOffset + (rtl ? 0 : 1) * maxWidthToCounterscroll, 0) + (rtl ? 0 : -1) * srcCounterscrollOverflow;
|
||||
|
||||
// Do not shift an element clipped by the screen border further than its original scroll starting point.
|
||||
const elementOffsetForClamping = rtl
|
||||
? Math.max(unclampedExpandingScrollOffset, rootUnclampedExpandingScrollOffset)
|
||||
: Math.min(unclampedExpandingScrollOffset, rootUnclampedExpandingScrollOffset);
|
||||
|
||||
// Based on the scroll point selected, determine how far to offset scrolls to keep the option in visible range.
|
||||
// Higher .scrollLeft values make this non-zero and reflect when scroll has begun clipping the element.
|
||||
const elementOffsetFromBorder = rtl
|
||||
// RTL offset: "offsetRight" based on "scrollRight"
|
||||
? Math.max(this.option.div.offsetLeft + this.option.currentWidth - (elementOffsetForClamping + this.scrollContainer.offsetWidth), 0) // double-check this one.
|
||||
// LTR: based on scrollLeft offsetLeft
|
||||
: Math.max(elementOffsetForClamping - this.option.div.offsetLeft, 0);
|
||||
|
||||
// If the element is close enough to the border, don't offset beyond the element!
|
||||
// If it is further, do not add excess padding - it'd effectively break scrolling.
|
||||
// Do maintain any remaining scroll offset that exists, though.
|
||||
const clampedExpandingScrollOffset = Math.min(maxWidthToCounterscroll, elementOffsetFromBorder);
|
||||
|
||||
const finalScrollOffset = unclampedExpandingScrollOffset // base scroll-coordinate transform mapping based on extra width from element expansion
|
||||
+ (rtl ? 1 : -1) * clampedExpandingScrollOffset // offset to scroll to put word-start border against the corresponding screen border, fully visible
|
||||
+ (rtl ? 0 : 1) * srcCounterscrollOverflow; // offset to maintain original overflow past that border if it existed
|
||||
|
||||
// -- Final step: Apply & fine-tune the final scroll positioning --
|
||||
this.scrollContainer.scrollLeft = finalScrollOffset;
|
||||
|
||||
// Prevent "jitters" during counterscroll that occur on expansion / collapse animation.
|
||||
// A one-frame "error correction" effect at the end of animation is far less jarring.
|
||||
if(this.pendingAnimation) {
|
||||
// scrollLeft doesn't work well with fractional values, unlike marginLeft / marginRight
|
||||
const fractionalOffset = this.scrollContainer.scrollLeft - finalScrollOffset;
|
||||
// So we put the fractional difference into marginLeft to force it to sync.
|
||||
this.option.currentWidth += fractionalOffset;
|
||||
}
|
||||
}
|
||||
|
||||
public decouple() {
|
||||
this.cancel();
|
||||
this.scrollContainer = null;
|
||||
}
|
||||
|
||||
private clear() {
|
||||
this.startTimestamp = null;
|
||||
window.cancelAnimationFrame(this.pendingAnimation);
|
||||
this.pendingAnimation = null;
|
||||
}
|
||||
|
||||
cancel() {
|
||||
this.clear();
|
||||
this.option.currentWidth = this.option.collapsedWidth;
|
||||
}
|
||||
|
||||
public expand() {
|
||||
// Cancel any prior iterating animation-frame commands.
|
||||
this.clear();
|
||||
|
||||
// set timestamp, adjusting the current time based on intermediate progress
|
||||
this.startTimestamp = performance.now();
|
||||
|
||||
let progress = this.option.currentWidth - this.option.collapsedWidth;
|
||||
let expansionDiff = this.option.expandedWidth - this.option.collapsedWidth;
|
||||
|
||||
if(progress != 0) {
|
||||
// Offset the timestamp by noting what start time would have given rise to
|
||||
// the current position, keeping related animations smooth.
|
||||
this.startTimestamp -= (progress / expansionDiff) * SuggestionExpandContractAnimation.TRANSITION_TIME;
|
||||
}
|
||||
|
||||
this.pendingAnimation = window.requestAnimationFrame(this._expand);
|
||||
}
|
||||
|
||||
private _expand = (timestamp: number) => {
|
||||
if(this.startTimestamp === undefined) {
|
||||
return; // No active expand op exists. May have been cancelled via `clear`.
|
||||
}
|
||||
|
||||
let progressTime = timestamp - this.startTimestamp;
|
||||
let fin = progressTime > SuggestionExpandContractAnimation.TRANSITION_TIME;
|
||||
|
||||
if(fin) {
|
||||
progressTime = SuggestionExpandContractAnimation.TRANSITION_TIME;
|
||||
}
|
||||
|
||||
// -- Part 1: handle option expand / collapse state --
|
||||
let expansionDiff = this.option.expandedWidth - this.option.collapsedWidth;
|
||||
let expansionRatio = progressTime / SuggestionExpandContractAnimation.TRANSITION_TIME;
|
||||
|
||||
// expansionDiff * expansionRatio: the total adjustment from 'collapsed' width, in px.
|
||||
const expansionPx = expansionDiff * expansionRatio;
|
||||
this.option.currentWidth = expansionPx + this.option.collapsedWidth;
|
||||
|
||||
// Part 2: trigger the next animation frame.
|
||||
if(!fin) {
|
||||
this.pendingAnimation = window.requestAnimationFrame(this._expand);
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
// Part 3: perform any needed counter-scrolling, scroll clamping, etc
|
||||
// Existence of a followup animation frame is part of the logic, so keep this 'after'!
|
||||
this.setScrollOffset();
|
||||
};
|
||||
|
||||
public collapse() {
|
||||
// Cancel any prior iterating animation-frame commands.
|
||||
this.clear();
|
||||
|
||||
// set timestamp, adjusting the current time based on intermediate progress
|
||||
this.startTimestamp = performance.now();
|
||||
|
||||
let progress = this.option.expandedWidth - this.option.currentWidth;
|
||||
let expansionDiff = this.option.expandedWidth - this.option.collapsedWidth;
|
||||
|
||||
if(progress != 0) {
|
||||
// Offset the timestamp by noting what start time would have given rise to
|
||||
// the current position, keeping related animations smooth.
|
||||
this.startTimestamp -= (progress / expansionDiff) * SuggestionExpandContractAnimation.TRANSITION_TIME;
|
||||
}
|
||||
|
||||
this.pendingAnimation = window.requestAnimationFrame(this._collapse);
|
||||
}
|
||||
|
||||
private _collapse = (timestamp: number) => {
|
||||
if(this.startTimestamp === undefined) {
|
||||
return; // No active collapse op exists. May have been cancelled via `clear`.
|
||||
}
|
||||
|
||||
let progressTime = timestamp - this.startTimestamp;
|
||||
let fin = progressTime > SuggestionExpandContractAnimation.TRANSITION_TIME;
|
||||
if(fin) {
|
||||
progressTime = SuggestionExpandContractAnimation.TRANSITION_TIME;
|
||||
}
|
||||
|
||||
// -- Part 1: handle option expand / collapse state --
|
||||
let expansionDiff = this.option.expandedWidth - this.option.collapsedWidth;
|
||||
let expansionRatio = 1 - progressTime / SuggestionExpandContractAnimation.TRANSITION_TIME;
|
||||
|
||||
// expansionDiff * expansionRatio: the total adjustment from 'collapsed' width, in px.
|
||||
const expansionPx = expansionDiff * expansionRatio;
|
||||
this.option.currentWidth = expansionPx + this.option.collapsedWidth;
|
||||
|
||||
// Part 2: trigger the next animation frame.
|
||||
if(!fin) {
|
||||
this.pendingAnimation = window.requestAnimationFrame(this._collapse);
|
||||
} else {
|
||||
this.clear();
|
||||
}
|
||||
|
||||
// Part 3: perform any needed counter-scrolling, scroll clamping, etc
|
||||
// Existence of a followup animation frame is part of the logic, so keep this 'after'!
|
||||
this.setScrollOffset();
|
||||
};
|
||||
}
|
||||
|
||||
|
|
|
|||
51
web/src/engine/osk/src/keyboard-layout/getTextMetrics.ts
Normal file
51
web/src/engine/osk/src/keyboard-layout/getTextMetrics.ts
Normal file
|
|
@ -0,0 +1,51 @@
|
|||
import { getFontSizeStyle } from "../fontSizeUtils.js";
|
||||
|
||||
let metricsCanvas: HTMLCanvasElement;
|
||||
|
||||
/**
|
||||
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
||||
*
|
||||
* @param {String} text The text to be rendered.
|
||||
* @param emScale The absolute `px` size expected to match `1em`.
|
||||
* @param {String} style The CSSStyleDeclaration for an element to measure against, without modification.
|
||||
*
|
||||
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
||||
* This version has been substantially modified to work for this particular application.
|
||||
*/
|
||||
export function getTextMetrics(text: string, emScale: number, style: {fontFamily?: string, fontSize: string}): TextMetrics {
|
||||
// Since we may mutate the incoming style, let's make sure to copy it first.
|
||||
// Only the relevant properties, though.
|
||||
style = {
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize
|
||||
};
|
||||
|
||||
// A final fallback - having the right font selected makes a world of difference.
|
||||
if(!style.fontFamily) {
|
||||
style.fontFamily = getComputedStyle(document.body).fontFamily;
|
||||
}
|
||||
|
||||
if(!style.fontSize || style.fontSize == "") {
|
||||
style.fontSize = '1em';
|
||||
}
|
||||
|
||||
let fontFamily = style.fontFamily;
|
||||
let fontSpec = getFontSizeStyle(style.fontSize);
|
||||
|
||||
var fontSize: string;
|
||||
if(fontSpec.absolute) {
|
||||
// We've already got an exact size - use it!
|
||||
fontSize = fontSpec.val + 'px';
|
||||
} else {
|
||||
fontSize = fontSpec.val * emScale + 'px';
|
||||
}
|
||||
|
||||
// re-use canvas object for better performance
|
||||
metricsCanvas = metricsCanvas ?? document.createElement("canvas");
|
||||
|
||||
var context = metricsCanvas.getContext("2d");
|
||||
context.font = fontSize + " " + fontFamily;
|
||||
var metrics = context.measureText(text);
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
|
@ -11,6 +11,7 @@ import buttonClassNames from '../buttonClassNames.js';
|
|||
|
||||
import { KeyElement } from '../keyElement.js';
|
||||
import VisualKeyboard from '../visualKeyboard.js';
|
||||
import { getTextMetrics } from './getTextMetrics.js';
|
||||
|
||||
/**
|
||||
* Replace default key names by special font codes for modifier keys
|
||||
|
|
@ -166,53 +167,6 @@ export default abstract class OSKKey {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Uses canvas.measureText to compute and return the width of the given text of given font in pixels.
|
||||
*
|
||||
* @param {String} text The text to be rendered.
|
||||
* @param {String} style The CSSStyleDeclaration for an element to measure against, without modification.
|
||||
*
|
||||
* @see https://stackoverflow.com/questions/118241/calculate-text-width-with-javascript/21015393#21015393
|
||||
* This version has been substantially modified to work for this particular application.
|
||||
*/
|
||||
static getTextMetrics(text: string, emScale: number, style: {fontFamily?: string, fontSize: string}): TextMetrics {
|
||||
// Since we may mutate the incoming style, let's make sure to copy it first.
|
||||
// Only the relevant properties, though.
|
||||
style = {
|
||||
fontFamily: style.fontFamily,
|
||||
fontSize: style.fontSize
|
||||
};
|
||||
|
||||
// A final fallback - having the right font selected makes a world of difference.
|
||||
if(!style.fontFamily) {
|
||||
style.fontFamily = getComputedStyle(document.body).fontFamily;
|
||||
}
|
||||
|
||||
if(!style.fontSize || style.fontSize == "") {
|
||||
style.fontSize = '1em';
|
||||
}
|
||||
|
||||
let fontFamily = style.fontFamily;
|
||||
let fontSpec = getFontSizeStyle(style.fontSize);
|
||||
|
||||
var fontSize: string;
|
||||
if(fontSpec.absolute) {
|
||||
// We've already got an exact size - use it!
|
||||
fontSize = fontSpec.val + 'px';
|
||||
} else {
|
||||
fontSize = fontSpec.val * emScale + 'px';
|
||||
}
|
||||
|
||||
// re-use canvas object for better performance
|
||||
var canvas: HTMLCanvasElement = OSKKey.getTextMetrics['canvas'] ||
|
||||
(OSKKey.getTextMetrics['canvas'] = document.createElement("canvas"));
|
||||
var context = canvas.getContext("2d");
|
||||
context.font = fontSize + " " + fontFamily;
|
||||
var metrics = context.measureText(text);
|
||||
|
||||
return metrics;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate the font size required for a key cap, scaling to fit longer text
|
||||
* @param vkbd
|
||||
|
|
@ -243,7 +197,7 @@ export default abstract class OSKKey {
|
|||
}
|
||||
|
||||
let fontSpec = getFontSizeStyle(style.fontSize || '1em');
|
||||
let metrics = OSKKey.getTextMetrics(text, emScale, style);
|
||||
let metrics = getTextMetrics(text, emScale, style);
|
||||
|
||||
const MAX_X_PROPORTION = 0.90;
|
||||
const MAX_Y_PROPORTION = 0.90;
|
||||
|
|
@ -300,12 +254,8 @@ export default abstract class OSKKey {
|
|||
// Add OSK key labels
|
||||
let keyText = null;
|
||||
if(spec['text'] == null || spec['text'] == '') {
|
||||
if(typeof spec['id'] == 'string') {
|
||||
// If the ID's Unicode-based, just use that code.
|
||||
keyText = ActiveKey.unicodeIDToText(spec['id']);
|
||||
}
|
||||
|
||||
keyText = keyText || DEFAULT_BLANK;
|
||||
// U_ codes are handled during keyboard pre-processing.
|
||||
keyText = DEFAULT_BLANK;
|
||||
} else {
|
||||
keyText=spec['text'];
|
||||
|
||||
|
|
@ -355,7 +305,7 @@ export default abstract class OSKKey {
|
|||
|
||||
// Check the key's display width - does the key visualize well?
|
||||
let emScale = vkbd.getKeyEmFontSize();
|
||||
var width: number = OSKKey.getTextMetrics(keyText, emScale, styleSpec).width;
|
||||
var width: number = getTextMetrics(keyText, emScale, styleSpec).width;
|
||||
if(width == 0 && keyText != '' && keyText != '\xa0') {
|
||||
// Add the Unicode 'empty circle' as a base support for needy diacritics.
|
||||
|
||||
|
|
|
|||
|
|
@ -627,6 +627,7 @@ export default abstract class OSKView
|
|||
if(!pending) {
|
||||
this.headerView?.refreshLayout();
|
||||
this.bannerView.refreshLayout();
|
||||
this.bannerView.width = this.computedWidth;
|
||||
this.footerView?.refreshLayout();
|
||||
}
|
||||
|
||||
|
|
@ -728,9 +729,7 @@ export default abstract class OSKView
|
|||
// Add suggestion banner bar to OSK
|
||||
this._Box.appendChild(this.banner.element);
|
||||
|
||||
if(this.bannerView.banner) {
|
||||
this.banner.banner.configureForKeyboard(this.keyboardData?.keyboard, this.keyboardData?.metadata);
|
||||
}
|
||||
this.bannerController?.configureForKeyboard(this.keyboardData?.keyboard, this.keyboardData?.metadata);
|
||||
|
||||
let kbdView: KeyboardView = this.keyboardView = this._GenerateKeyboardView(this.keyboardData?.keyboard, this.keyboardData?.metadata);
|
||||
this._Box.appendChild(kbdView.element);
|
||||
|
|
|
|||
|
|
@ -407,6 +407,20 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
previewHost: GesturePreviewHost
|
||||
}> = {};
|
||||
|
||||
const clearActiveGestures = (excludedTouchpointId?: string) => {
|
||||
for(const identifier of Object.keys(sourceTrackingMap)) {
|
||||
// Filter out the exclusion if one exists.
|
||||
if(identifier == excludedTouchpointId) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Any _other_ gesture, though - yeah, that should cancel out.
|
||||
// Note: this can cancel ongoing modipress gestures, which may trigger an unexpected layer shift.
|
||||
const entry = sourceTrackingMap[identifier];
|
||||
entry.source.terminate(true);
|
||||
}
|
||||
}
|
||||
|
||||
const gestureHandlerMap = new Map<GestureSequence<KeyElement>, GestureHandler[]>();
|
||||
|
||||
// Now to set up event-handling links.
|
||||
|
|
@ -612,8 +626,11 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
// Merely constructing the instance is enough; it'll link into the sequence's events and
|
||||
// handle everything that remains for the backspace from here.
|
||||
handlers = [new HeldRepeater(gestureSequence, () => this.modelKeyClick(gestureKey, coord))];
|
||||
} else if(gestureKey.key.spec.baseKeyID == "K_LOPT") {
|
||||
} else if(gestureKey.key.spec.baseKeyID == "K_LOPT") { // globe key
|
||||
gestureSequence.on('complete', () => this.emit('globekey', gestureKey, false));
|
||||
// Cancel all other gesture sources; a language-menu interaction voids all previously-active
|
||||
// gestures that haven't completed.
|
||||
clearActiveGestures(coordSource.identifier);
|
||||
}
|
||||
} else if(gestureStage.matchedId.indexOf('longpress') > -1) {
|
||||
existingPreviewHost?.cancel();
|
||||
|
|
@ -903,7 +920,7 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
|
||||
// Prevent NaN breakages.
|
||||
if (!width || !height) {
|
||||
return null;
|
||||
return new Map();
|
||||
}
|
||||
|
||||
let kbdAspectRatio = width / height;
|
||||
|
|
@ -1038,7 +1055,9 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
return;
|
||||
}
|
||||
|
||||
this.gestureEngine.stateToken = layerId;
|
||||
if(this.gestureEngine) {
|
||||
this.gestureEngine.stateToken = layerId;
|
||||
}
|
||||
|
||||
// So... through KMW 14, we actually never tracked the capsKey, numKey, and scrollKey
|
||||
// properly for keyboard-defined layouts - only _default_, desktop-style layouts.
|
||||
|
|
@ -1250,15 +1269,19 @@ export default class VisualKeyboard extends EventEmitter<EventMap> implements Ke
|
|||
return;
|
||||
}
|
||||
|
||||
// Step 3: perform layout operations.
|
||||
const paddingZone = this.gestureEngine.config.maxRoamingBounds as PaddedZoneSource;
|
||||
paddingZone.updatePadding([-0.333 * this.currentLayer.rowHeight]);
|
||||
// Step 3: recalculate gesture parameter values
|
||||
// Skip for doc-keyboards, since they don't do gestures.
|
||||
if(!this.isStatic) {
|
||||
const paddingZone = this.gestureEngine.config.maxRoamingBounds as PaddedZoneSource;
|
||||
paddingZone.updatePadding([-0.333 * this.currentLayer.rowHeight]);
|
||||
|
||||
this.gestureParams.longpress.flickDist = 0.25 * this.currentLayer.rowHeight;
|
||||
this.gestureParams.flick.startDist = 0.15 * this.currentLayer.rowHeight;
|
||||
this.gestureParams.flick.dirLockDist = 0.35 * this.currentLayer.rowHeight;
|
||||
this.gestureParams.flick.triggerDist = 0.75 * this.currentLayer.rowHeight;
|
||||
this.gestureParams.longpress.flickDist = 0.25 * this.currentLayer.rowHeight;
|
||||
this.gestureParams.flick.startDist = 0.15 * this.currentLayer.rowHeight;
|
||||
this.gestureParams.flick.dirLockDist = 0.35 * this.currentLayer.rowHeight;
|
||||
this.gestureParams.flick.triggerDist = 0.75 * this.currentLayer.rowHeight;
|
||||
}
|
||||
|
||||
// Step 4: perform layout operations.
|
||||
// Needs the refreshed layout info to work correctly.
|
||||
if(this.currentLayer) {
|
||||
this.currentLayer.refreshLayout(this, this._computedHeight - this.getVerticalLayerGroupPadding());
|
||||
|
|
|
|||
|
|
@ -66,6 +66,25 @@
|
|||
font-size: 0.75em;
|
||||
}
|
||||
|
||||
.kmw-suggest-banner-scroller {
|
||||
overflow-x: hidden;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
scrollbar-width: none; /* Firefox scrollbar prevention */
|
||||
}
|
||||
|
||||
.kmw-suggest-banner-scroller::-webkit-scrollbar {
|
||||
display: none; /* Safari + Chrome scrollbar prevention */
|
||||
}
|
||||
|
||||
.kmw-suggest-option {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.kmw-suggestion-container {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.phone.windows .kmw-key-row{max-width:80%;}
|
||||
|
||||
.phone .kmw-5rows {padding-top: 0;}
|
||||
|
|
@ -81,7 +100,6 @@
|
|||
.phone.ios .kmw-key.kmw-key-shift-on,
|
||||
.phone.ios .kmw-key.kmw-key-special-on {color:#fff;background-color:#88f;}
|
||||
.phone.ios .kmw-key.kmw-key-touched {background-color:#447;}
|
||||
.phone.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;}
|
||||
.phone.ios .kmw-key-deadkey{color:#048204;background-color:#fdfdfe;}
|
||||
|
||||
/* Probably best to make this its own CSS that can be optionally included? */
|
||||
|
|
@ -104,6 +122,14 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.ios .kmw-suggest-option::before {
|
||||
background: linear-gradient(90deg, #cfd3d9 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.ios .kmw-suggest-option::after {
|
||||
background: linear-gradient(90deg, transparent 0%, #cfd3d9 100%);
|
||||
}
|
||||
|
||||
.ios .kmw-banner-bar .kmw-suggest-option {
|
||||
display:inline-block;
|
||||
text-align: center;
|
||||
|
|
@ -118,7 +144,18 @@
|
|||
color: #000;
|
||||
}
|
||||
|
||||
.phone.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;}
|
||||
.phone.ios .kmw-suggest-option.kmw-suggest-touched,
|
||||
.tablet.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;}
|
||||
|
||||
.phone.ios .kmw-suggest-option.kmw-suggest-touched::before,
|
||||
.tablet.ios .kmw-suggest-option.kmw-suggest-touched::before {
|
||||
background: linear-gradient(90deg, #88f 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.phone.ios .kmw-suggest-option.kmw-suggest-touched::after,
|
||||
.tablet.ios .kmw-suggest-option.kmw-suggest-touched::after {
|
||||
background: linear-gradient(90deg, transparent 0%, #88f 100%);
|
||||
}
|
||||
|
||||
.phone.ios.kmw-osk-frame,
|
||||
.tablet.ios.kmw-osk-frame {
|
||||
|
|
@ -136,6 +173,14 @@
|
|||
background-color: #0f1319;
|
||||
}
|
||||
|
||||
.ios .kmw-suggest-option::before {
|
||||
background: linear-gradient(90deg, #0f1319 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.ios .kmw-suggest-option::after {
|
||||
background: linear-gradient(90deg, transparent 0%, #0f1319 100%);
|
||||
}
|
||||
|
||||
.ios .kmw-banner-bar .kmw-banner-separator {
|
||||
border-left: solid 1px #8a8d90
|
||||
}
|
||||
|
|
@ -174,6 +219,14 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.phone.android .kmw-suggest-option::before {
|
||||
background: linear-gradient(90deg, #222 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.phone.android .kmw-suggest-option::after {
|
||||
background: linear-gradient(90deg, transparent 0%, #222 100%);
|
||||
}
|
||||
|
||||
.phone.android .kmw-banner-bar .kmw-suggest-option {
|
||||
display:inline-block;
|
||||
text-align: center;
|
||||
|
|
@ -186,6 +239,14 @@
|
|||
|
||||
.phone.android .kmw-suggest-option.kmw-suggest-touched {background-color:#bbb;}
|
||||
|
||||
.phone.android .kmw-suggest-option.kmw-suggest-touched::before {
|
||||
background: linear-gradient(90deg, #bbb 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.phone.android .kmw-suggest-option.kmw-suggest-touched::after {
|
||||
background: linear-gradient(90deg, transparent 0%, #bbb 100%);
|
||||
}
|
||||
|
||||
.tablet.kmw-osk-frame{left:0;bottom:0;width:100%;height:144px;overflow-y:visible;
|
||||
background-color:rgba(0,0,0,0.8);-webkit-user-select:none;}
|
||||
.tablet .kmw-osk-inner-frame{margin:0;background:transparent;}
|
||||
|
|
@ -209,7 +270,6 @@
|
|||
.tablet.ios .kmw-key.kmw-key-shift-on,
|
||||
.tablet.ios .kmw-key.kmw-key-special-on {color:#fff;background-color:#88f;}
|
||||
.tablet.ios .kmw-key.kmw-key-touched {background-color:#447;}
|
||||
.tablet.ios .kmw-suggest-option.kmw-suggest-touched {background-color:#88f;}
|
||||
.tablet.ios .kmw-key-deadkey{color:#048204;background-color:#fdfdfe;}
|
||||
|
||||
/* Probably best to make this its own CSS that can be optionally included? */
|
||||
|
|
@ -249,6 +309,14 @@
|
|||
width: 100%;
|
||||
}
|
||||
|
||||
.tablet.android .kmw-suggest-option::before {
|
||||
background: linear-gradient(90deg, #b4b4b8 0px, transparent 100%);
|
||||
}
|
||||
|
||||
.tablet.android .kmw-suggest-option::after {
|
||||
background: linear-gradient(90deg, transparent 0%, #b4b4b8 100%);
|
||||
}
|
||||
|
||||
.tablet.android .kmw-banner-bar .kmw-suggest-option {
|
||||
display:inline-block;
|
||||
text-align: center;
|
||||
|
|
@ -258,11 +326,20 @@
|
|||
.tablet.android .kmw-suggestion-text {
|
||||
color:#77f;
|
||||
}
|
||||
|
||||
.tablet.android .kmw-suggest-option.kmw-suggest-touched {background-color:#447;}
|
||||
|
||||
.tablet.android .kmw-suggest-option.kmw-suggest-touched::before {
|
||||
background: linear-gradient(90deg, #447 0px, transparent 100%);
|
||||
}
|
||||
|
||||
.tablet.android .kmw-suggest-option.kmw-suggest-touched::after {
|
||||
background: linear-gradient(90deg, transparent 0%, #447 100%);
|
||||
}
|
||||
|
||||
/* Vertical centering of text labels on keys */
|
||||
.kmw-key {text-align:center; white-space:nowrap;}
|
||||
.kmw-key:before {content:'.'; display:inline-block; height:100%; vertical-align:middle; max-width:0px; visibility:hidden;}
|
||||
.kmw-key::before {content:'.'; display:inline-block; height:100%; vertical-align:middle; max-width:0px; visibility:hidden;}
|
||||
.kmw-key span {display:inline-block}
|
||||
|
||||
|
||||
|
|
@ -309,22 +386,98 @@
|
|||
.kmw-footer-caption{color:#fff;font:0.7em Arial;margin:0 0 0 4px;}
|
||||
|
||||
.kmw-banner-bar{height:100%; width:100%; margin:0; background-color:darkorange; display: inline-block; white-space: nowrap;}
|
||||
|
||||
/* Creates a gradient to fade text at the borders, providing visual indication of overflow */
|
||||
/* Make sure the non-transparent color of the gradient matches .kmw-banner-bar's background-color. */
|
||||
.kmw-suggest-option::before,
|
||||
.kmw-suggest-option::after {
|
||||
position:absolute;
|
||||
|
||||
/* Set scrollable-suggestion fade width here. Make sure to also set .kmw-suggestion-text
|
||||
* padding-left and padding-right accordingly!
|
||||
*/
|
||||
width: 32px;
|
||||
height: 100%;
|
||||
content: '';
|
||||
top: 0;
|
||||
z-index:10000; /* z-indexes this _behind_ the 'option' element that hosts the scrollable zone. */
|
||||
user-select: none;
|
||||
pointer-events: none; /* Ensures click-through! But apparently not touch-through. */
|
||||
touch-action: none; /* Doesn't seem to allow touch-through, though - even with touch-action: none */
|
||||
/* https://stackoverflow.com/q/21474722 - poster never could find a solution, and settled*/
|
||||
/* on the same workaround: a 'before' and 'after' piece instead of a single overlay.*/
|
||||
transition: opacity 0.25s linear;
|
||||
}
|
||||
|
||||
.kmw-suggest-option.swallow-fade-transition::before,
|
||||
.kmw-suggest-option.swallow-fade-transition::after {
|
||||
transition-duration: 0s;
|
||||
}
|
||||
|
||||
.kmw-suggest-option::before {
|
||||
background: linear-gradient(90deg, darkorange 0%, transparent 100%);
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.kmw-suggest-option.kmw-hide-fade-left::before,
|
||||
.kmw-suggest-option.kmw-hide-fade-right::after {
|
||||
opacity: 0;
|
||||
/* visibility: hidden; */
|
||||
}
|
||||
|
||||
.kmw-suggest-option::after {
|
||||
background: linear-gradient(90deg, transparent 0%, darkorange 100%);
|
||||
right: 0;
|
||||
}
|
||||
|
||||
/* Fallback suggestion-selection highlighting */
|
||||
.kmw-suggest-option.kmw-suggest-touched {
|
||||
background: #bbb;
|
||||
}
|
||||
|
||||
.kmw-suggest-option.kmw-suggest-touched::before,
|
||||
.kmw-suggest-option.kmw-suggest-touched::after {
|
||||
/* Immediately start hiding the fade styling for touched suggestions. */
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
/* Creates a gradient to fade text at the borders, providing visual indication of overflow */
|
||||
/* Make sure the non-transparent color of the gradient matches .kmw-banner-bar's background-color. */
|
||||
.kmw-suggest-option.kmw-suggest-touched::before {
|
||||
background: linear-gradient(90deg, #bbb 0%, transparent 100%);
|
||||
}
|
||||
|
||||
.kmw-suggest-option.kmw-suggest-touched::after {
|
||||
background: linear-gradient(90deg, transparent 0%, #bbb 100%);
|
||||
}
|
||||
|
||||
.kmw-banner-bar .kmw-banner-separator {border-left: solid 1px #8a8d90; width: 0px; vertical-align: middle; height: 45%; display: inline-block;}
|
||||
.kmw-banner-bar .kmw-suggest-option {display:inline-block; text-align: center; height: 85%; overflow-x:hidden}
|
||||
.kmw-suggestion-text{color:#fff; line-height: normal; position: relative; vertical-align: middle;}
|
||||
.kmw-banner-bar .kmw-suggest-option {display:inline-block; text-align: center; height: 85%; position: relative; z-index: 10001}
|
||||
.kmw-suggestion-text {
|
||||
color:#fff;
|
||||
line-height: normal;
|
||||
position: relative;
|
||||
vertical-align: middle;
|
||||
/* Contrast with .kmw-suggest-option::before .width styling. */
|
||||
padding-left: 8px; /* Keeps a bit of whitespace on the suggestion's side. */
|
||||
padding-right: 8px; /* Keeps a bit of whitespace on the suggestion's side. */
|
||||
width: max-content; /* Ensure the text span acts like it contains its text */
|
||||
min-width: calc(100% - 16px); /* To ensure the span stays centered. */
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kmw-footer-resize{cursor:se-resize;position:absolute;right:2px;bottom:2px;width:16px;height:16px;overflow:hidden;
|
||||
font-family:SpecialOSK;color:white;}
|
||||
.kmw-footer-resize:hover{font-weight:bold;}
|
||||
.kmw-footer-resize:before {content:'\e023';}
|
||||
.kmw-footer-resize::before {content:'\e023';}
|
||||
|
||||
.kmw-title-bar-image {cursor: default; float:right; padding: 2px 2px 0 0; width:16px; height:16px;
|
||||
font-family:SpecialOSK; color:white;}
|
||||
.kmw-title-bar-image:hover{font-weight:bold;}
|
||||
#kmw-pin-image:before{content:'\e024';}
|
||||
#kmw-config-image:before{content:'\e030';}
|
||||
#kmw-help-image:before{content:'\e042';}
|
||||
#kmw-close-button:before {content:'\e025';}
|
||||
#kmw-pin-image::before{content:'\e024';}
|
||||
#kmw-config-image::before{content:'\e030';}
|
||||
#kmw-help-image::before{content:'\e042';}
|
||||
#kmw-close-button::before {content:'\e025';}
|
||||
|
||||
/* Common key appearance styles (can override with form-factor styles if necessary) */
|
||||
.kmw-key-default{color:#000;background-color:#eee;}
|
||||
|
|
@ -505,7 +658,7 @@ div.android div.kmw-keytip-cap {
|
|||
/* Box styles for keyboard-specific OSK (e.g. EuroLatin) and if no keyboard active (desktop only) */
|
||||
.kmw-osk-static, .kmw-osk-none{text-align:left;font:12px sans-serif;border:solid 1px #ad4a28;color:blue;background-color:white;}
|
||||
.kmw-osk-none{padding:4px 6px 6px}
|
||||
.kmw-osk-none:before{content:'Installing keyboard...';}
|
||||
.kmw-osk-none::before{content:'Installing keyboard...';}
|
||||
|
||||
/* OSK language menu styles */
|
||||
#kmw-language-menu{position:fixed;left:0;width:232px;max-width:232px;z-index:10004;background-color:rgba(128,128,128,1);
|
||||
|
|
@ -619,7 +772,7 @@ div.android div.kmw-keytip-cap {
|
|||
border:3px solid #ad4a28;border-radius:8px;text-align:center;padding:0px;background:white;}
|
||||
.kmw-alert-close{float:right; height:24px; width:24px; font:1em bold Arial,sans-serif;color:#ad4a28;}
|
||||
/*.kmw-alert-close{float:right; height:24px; width:24px; font:2em bold Arial,sans-serif;color:#ad4a28;} */
|
||||
.kmw-alert-close:before{content:'\00d7'}
|
||||
.kmw-alert-close::before{content:'\00d7'}
|
||||
/*.kmw-alert-close{float:right;background:url('icons.gif') no-repeat -30px 0; height:13px; width:15px;}*/
|
||||
.kmw-wait-text{clear:both; margin:4px;white-space:nowrap;}
|
||||
.kmw-wait-graphic{width:100%;min-height:19px;background:url('ajax-loader.gif') no-repeat;background-position:center top;}
|
||||
|
|
|
|||
|
|
@ -48,9 +48,12 @@
|
|||
kmw.addKeyboards({id:'gesture_prototyping',name:'Gesture prototyping',languages:{id:'en',name:'English'},
|
||||
filename:('../keyboards/gesture_prototyping/build/gesture_prototyping.js')});
|
||||
|
||||
var pageRef = (window.location.protocol == 'file:')
|
||||
? window.location.href.substr(0, window.location.href.lastIndexOf('/')+1)
|
||||
: window.location.href;
|
||||
// Ensure the URL we prefix to the page's path is the page's directory, not including
|
||||
// the actual HTML page itself.
|
||||
var urlIncludesIndex = window.location.href.lastIndexOf('.html') > 1;
|
||||
var pageRef = (urlIncludesIndex
|
||||
? window.location.href.substr(0, window.location.href.lastIndexOf('/'))
|
||||
: window.location.href) + '/';
|
||||
|
||||
var modelStub = {'id': 'nrc.en.mtnt',
|
||||
languages: ['en'],
|
||||
|
|
|
|||
|
|
@ -46,9 +46,12 @@
|
|||
kmw.addKeyboards({id:'obolo_chwerty_6351',name:'obolo_chwerty_6351',languages:{id:'en',name:'English'},
|
||||
filename:('../obolo_chwerty_6351.js')});
|
||||
|
||||
var pageRef = (window.location.protocol == 'file:')
|
||||
? window.location.href.substr(0, window.location.href.lastIndexOf('/')+1)
|
||||
: window.location.href;
|
||||
// Ensure the URL we prefix to the page's path is the page's directory, not including
|
||||
// the actual HTML page itself.
|
||||
var urlIncludesIndex = window.location.href.lastIndexOf('.html') > 1;
|
||||
var pageRef = (urlIncludesIndex
|
||||
? window.location.href.substr(0, window.location.href.lastIndexOf('/'))
|
||||
: window.location.href) + '/';
|
||||
|
||||
var modelStub = {'id': 'example.en.trie',
|
||||
languages: ['en'],
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ This renderer loads all the cloud keyboards from api.keyman.com and renders each
|
|||
- Note that it is preferable to run this on an actual device if possible.
|
||||
- If no prompt is given re: screensharing when you click the 'run' button, use Chrome's Developer Tools
|
||||
on a desktop or laptop to run this via emulation instead.
|
||||
- If emulating a mobile device, when prompted to screenshare, be sure to share _the Chrome tab_. The screen capture
|
||||
system will fail to capture the OSK properly otherwise.
|
||||
- If emulating a mobile device, when prompted to screenshare...
|
||||
- Be sure to share _the Chrome tab_. The screen capture system will fail to capture the OSK properly otherwise.
|
||||
- Also verify that emulation zoom is set to 100%; the system will fail to capture the OSK properly otherwise.
|
||||
6. Save the result to a .html file, either before.html or after.html.
|
||||
7. When swapping versions, don't forget to rebuild.
|
||||
|
||||
|
|
|
|||
|
|
@ -27,7 +27,7 @@
|
|||
<!-- Insert unminified KeymanWeb source scripts -->
|
||||
<!--<script src="https://s.keyman.com/kmw/engine/14.0.227/keymanweb.js" type="application/javascript"></script>-->
|
||||
<script src="../../../../build/publish/debug/keymanweb.js" type="application/javascript"></script>
|
||||
<script src="../../../../build/tools/testing/bulk_rendering/bulk_render.js" type="application/javascript"></script>
|
||||
<script src="../../../../build/tools/testing/bulk_rendering/lib/bulk_render.js" type="application/javascript"></script>
|
||||
|
||||
<!-- Initialization: set paths to keyboards, resources and fonts as required -->
|
||||
<script>
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
// Includes KeymanWeb's Device class, as it's quite a useful resource for KMW-related projects.
|
||||
import { Device } from 'keyman/engine/device-detect';
|
||||
import { type DeviceSpec } from '@keymanapp/web-utils';
|
||||
|
||||
import type { KeymanEngine } from 'keyman/app/browser';
|
||||
import type { FloatingOSKView } from 'keyman/engine/osk';
|
||||
|
|
@ -28,7 +29,10 @@ export class BatchRenderer {
|
|||
width: screen.width,
|
||||
height: screen.height,
|
||||
frameRate: 60,
|
||||
}
|
||||
},
|
||||
// Chrome 96+.
|
||||
// https://stackoverflow.com/a/70665376
|
||||
preferCurrentTab: true
|
||||
});
|
||||
} catch(err) {
|
||||
console.error("Error: " + err);
|
||||
|
|
@ -95,7 +99,7 @@ export class BatchRenderer {
|
|||
return capture();
|
||||
}
|
||||
|
||||
createKeyboardHeader(kbd: KeyboardData, loaded: boolean): HTMLDivElement {
|
||||
createKeyboardHeader(kbd: KeyboardData, formFactor: DeviceSpec.FormFactor, loaded: boolean): HTMLDivElement {
|
||||
let divHeader = document.createElement('div');
|
||||
let eleName = document.createElement('h2');
|
||||
|
||||
|
|
@ -108,7 +112,20 @@ export class BatchRenderer {
|
|||
|
||||
eleDescription.appendChild(document.createTextNode('Name: ' + kbd.Name));
|
||||
eleDescription.appendChild(document.createElement('br'));
|
||||
eleDescription.appendChild(document.createTextNode('Font: ' + keyman.core.activeKeyboard['_legacyLayoutSpec'].F));
|
||||
|
||||
const keyboard = keyman.core.activeKeyboard;
|
||||
// Some keyboards, such as sil_euro_latin and sil_ipa, no longer specify this property.
|
||||
if(keyboard['_legacyLayoutSpec']) {
|
||||
eleDescription.appendChild(document.createTextNode('Font: ' + keyboard['_legacyLayoutSpec'].F));
|
||||
} else {
|
||||
// They instead specify only the post-KMW-10 touch-layout format.
|
||||
const layout = keyboard.layout(formFactor);
|
||||
if(!layout) {
|
||||
eleDescription.appendChild(document.createTextNode('Keyboard is help-text based'));
|
||||
} else {
|
||||
eleDescription.appendChild(document.createTextNode('Font: ' + layout.font));
|
||||
}
|
||||
}
|
||||
|
||||
} else {
|
||||
eleDescription.appendChild(document.createTextNode('Unable to load this keyboard!'));
|
||||
|
|
@ -121,7 +138,8 @@ export class BatchRenderer {
|
|||
|
||||
private processKeyboard(kbd: KeyboardData) {
|
||||
let p: Promise<any> = keyman.setActiveKeyboard(kbd.InternalName);
|
||||
let isMobile = keyman.config.hostDevice.formFactor != 'desktop';
|
||||
const formFactor = keyman.config.hostDevice.formFactor;
|
||||
let isMobile = formFactor != 'desktop';
|
||||
|
||||
// Establish common keyboard header info.
|
||||
let divSummary = document.createElement('div');
|
||||
|
|
@ -143,30 +161,35 @@ export class BatchRenderer {
|
|||
const screenOffsetY = window.outerHeight - window.innerHeight;
|
||||
BatchRenderer.boundingRect.y += screenOffsetY;
|
||||
|
||||
divSummary.appendChild(renderer.createKeyboardHeader(kbd, true));
|
||||
divSummary.appendChild(renderer.createKeyboardHeader(kbd, formFactor, true));
|
||||
|
||||
let divRenders = document.createElement('div');
|
||||
divSummary.appendChild(divRenders);
|
||||
|
||||
// Uses 'private' APIs that may be subject to change in the future. Keep it updated!
|
||||
var layers;
|
||||
if(isMobile) {
|
||||
layers = keyman.osk.vkbd.layerGroup.layers;
|
||||
} else {
|
||||
if(!isMobile) {
|
||||
// The desktop OSK will be overpopulated, with a number of blank layers to display in most cases.
|
||||
// We instead rely upon the KLS definition to ensure we keep the renders sparse.
|
||||
//
|
||||
// _legacyLayoutSpec is technically private, but it's what we've been using, so... yeah.
|
||||
layers = keyman.core.activeKeyboard['_legacyLayoutSpec'].KLS;
|
||||
layers = keyman.core.activeKeyboard['_legacyLayoutSpec']?.KLS;
|
||||
}
|
||||
|
||||
// If mobile, or if the desktop definition lacks a `_legacyLayoutSpec` entry.
|
||||
// Note: vkbd will be null for keyboards with desktop help-text, such as sil_euro_latin.
|
||||
layers = layers || keyman.osk.vkbd?.layerGroup.layers;
|
||||
|
||||
let renderLayer = function(i: number) {
|
||||
return new Promise(function(resolve) {
|
||||
// (Private API) Directly sets the keyboard layer within KMW, then uses .show to force-display it.
|
||||
if(keyman.osk.vkbd) {
|
||||
keyman.core.keyboardProcessor.layerId = Object.keys(layers)[i];
|
||||
} else {
|
||||
console.error("Error - keyman.osk.vkbd is undefined!");
|
||||
// Again, occurs for keyboards with desktop help-text.
|
||||
console.error(`Error - keyman.osk.vkbd is undefined for ${kbd.InternalName}!`);
|
||||
// Will otherwise crash the renderer later.
|
||||
return;
|
||||
}
|
||||
// Make sure the active element's still set!
|
||||
renderer.setActiveDummy();
|
||||
|
|
@ -192,10 +215,11 @@ export class BatchRenderer {
|
|||
};
|
||||
|
||||
// The resulting Promise will only call it's `.then()` once all of this keyboard's renders have been completed.
|
||||
return renderer.arrayPromiseIteration(renderLayer, Object.keys(layers).length);
|
||||
}).catch(function() {
|
||||
console.log("Failed to load the \"" + kbd.InternalName + "\" keyboard for rendering!");
|
||||
divSummary.appendChild(renderer.createKeyboardHeader(kbd, false));
|
||||
return renderer.arrayPromiseIteration(renderLayer, layers ? Object.keys(layers).length : 0);
|
||||
}).catch(function(err) {
|
||||
console.error("Failed to load the \"" + kbd.InternalName + "\" keyboard for rendering:");
|
||||
console.error(err);
|
||||
divSummary.appendChild(renderer.createKeyboardHeader(kbd, formFactor, false));
|
||||
return Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
|
@ -293,5 +317,7 @@ export class BatchRenderer {
|
|||
}
|
||||
|
||||
(function(){
|
||||
// BatchRenderer's internal state stuff is static on the class and is not exposed with
|
||||
// the line below.
|
||||
window['kmw_renderer'] = new BatchRenderer();
|
||||
})();
|
||||
Loading…
Add table
Reference in a new issue