diff --git a/common/predictive-text/README.md b/common/predictive-text/README.md
index 5f41cea69e..4b91416a6c 100644
--- a/common/predictive-text/README.md
+++ b/common/predictive-text/README.md
@@ -1,408 +1,61 @@
-Keyman/Language Modelling layer
-================================
+Language Modelling Layer (LMLayer)
+==================================
-This document introduces the protocol for communicating between
-KeymanWeb and the language modeling layer (i.e., the configurable
-prediction and suggestion engine, (henceforth referred to as the
-"LMLayer").
+Provide predictions and corrections while you type!
-Note: I'm using the term **keyboard** as a synonym for **KeymanWeb**.
+See [Worker Communication Protocol](./docs/worker-communication-protocol.md) for a
+semi-formal specification on how the Worker and the main thread communicate.
-> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
-> NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
-> "OPTIONAL" in this document are to be interpreted as described in
-> [RFC 2119].
+System dependencies
+-------------------
-[RFC 2119]: https://www.ietf.org/rfc/rfc2119.txt
+You will need Bash and Node.js >= 6.0.
+Build
+-----
-Communication protocol between keyboard and asynchronous worker
----------------------------------------------------------------
+Run `build.sh`. This will also automatically install dependencies with `npm`.
-
-
-We have decided that everything to the right of the `KeymanWeb` will be
-in a Web Worker. However, communication can happen only through
-[`postMessage(data)`][postMessage] commands,
-where `data` is a serializable object (via the [structured clone][]
-algorithm).
-
-What serializable object can we send that will adhere to the [open-closed principle]?
-
-### Messages
-
-The idea is to use a [discriminated union][]. The protocol involves
-plain JavaScript objects with one property called `message` that takes
-a finite set of `string` values.
-
-These string values indicate what message should be sent. The rest of
-the properties in the object are the parameters send with the message.
-
-```javascript
-{
- message: 'predict',
- // message-specific properties here
-}
+```sh
+./build.sh
```
-See also: [XML-RPC][]
+### Two-stage compilation process
-Messages are **not** methods. That is, there is no assumption that
-a client will receive a reply when a message is sent. However, some
-pairs of messages, such as `predict` and `suggestions`, lightly assume
-request/response semantics.
+Since the primary LMLayer code runs within a [Web Worker][], `build.sh` compiles the
+LMLayer in two stages:
-[discriminated union]: http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
-[open-closed principle]: https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle
-[XML-RPC]: https://en.wikipedia.org/wiki/XML-RPC
-[structured clone]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
-[postMessage]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
+ 1. Compile the inner worker code
+ 1. Compile the TypeScript sources for _only_ the Worker code.
+ 2. Wrap the Worker code as `embedded_worker.js`
+ 2. Compile the top-level code
+ 1. Include `embedded_worker.js` verbatim using a TypeScript directive.
+ 2. Compile the top-level TypeScript code.
+ 3. Unwrap the Worker code at runtime.
-### Tokens
+[Web Worker]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Using_web_workers
-Tokens uniquely identify an input event, such as a key press. Since
-Keyman should ask for an asynchronous prediction on most key presses, the
-token is intended to associate a prediction and its response with
-a particular input; Keyman is free to ignore prediction responses if
-they are for outdated input events.
+Test
+----
-The `Token` type is opaque to LMLayer. That is, LMLayer does not inspect
-its contents; it simply uses it to identify a request and pass it back
-to Keyman. There are a few requirements on the concrete type of the
-`Token`:
+This will run both headless unit tests, and in-browser unit tests and integration
+tests:
- 1. Tokens **MUST** be serializable via the [structured clone][] algorithm;
- 2. Tokens **MUST** be usable as a key in a [`Map`][Map object] object.
- 3. Tokens **MUST** be unique across messages. That is, tokens **MUST
- NOT** be duplicated between different messages.
-
-It is up to the keyboard to create unambiguous tokens that can be uniquely
-identified through the round-trip process.
-
-In the following examples, I'll use the subset of `number` values that
-are interpretable as [31-bit signed integers][Smi].
-
-[Map object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality
-[Smi]: https://github.com/thlorenz/v8-perf/blob/master/data-types.md#efficiently-representing-values-and-tagging
-[Tokens]: #tokens
-
-### Example
-
-An asynchronous message to predict after typing 'D':
-
-```javascript
-{
- message: 'predict',
-
- token: 1,
- transform: {
- insert: 'D',
- deleteLeft: 0,
- deleteRight: 0
- },
- context: {
- left: '',
- right: '',
- startOfBuffer: true,
- endOfBuffer: true
- },
-}
+```sh
+./build.sh -test
```
-Message types
--------------
+### Test-Driven Development
-Currently there are four message types:
+I like to use [entr]() to automatically build and re-run the unit tests anytime I
+change a source code file. Here's the command I run in separate window:
-Message | Direction | Parameters | Expected reply | Uses token
---------------|--------------------|---------------------|---------------------|---------------
-`initialize` | keyboard → LMLayer | initialization | Yes — `ready` | No
-`ready` | LMLayer → keyboard | configuration | No | No
-`predict` | keyboard → LMLayer | transform, context | Yes — `suggestions` | Yes
-`suggestions` | LMLayer → keyboard | suggestions | No | Yes
-
-
-### Message: `initialize`
-
-Must be sent from the keyboard to the LMLayer so that the LMLayer
-initializes a model. It will send `initialization` which is a plain
-JavaScript object specify the path to the model, as well configurations
-and platform restrictions.
-
-The keyboard **MUST NOT** send any messages to the LMLayer prior to
-sending `initialize`. The keyboard **SHOULD NOT** send another message
-to the keyboard until it receives `ready` message from the LMLayer
-before sending another message.
-
-These are the configurations, and platform restrictions sent to
-initialize the LMLayer and its model.
-
-```typescript
-interface InitializeMessage {
- message: 'initialize';
-
- /**
- * Path to the model. There are no concrete restrictions on the path
- * to the model, so long as the LMLayer can successfully use it to
- * initialize the model.
- */
- model: string;
-
- configuration: {
- /**
- * Whether the platform supports right contexts.
- * The absence of this rule implies false.
- */
- supportsRightContexts?: false,
-
- /**
- * Whether the platform supports deleting to the right.
- * The absence of this rule implies false.
- */
- supportsDeleteRight?: false,
-
- /**
- * The maximum amount of UTF-16 code units that the keyboard will
- * provide to the left of the cursor, as an integer.
- */
- maxLeftContextCodeUnits: number,
-
- /**
- * The maximum amount of code units that the keyboard will provide to
- * the right of the cursor, as an integer. The absence of this rule
- * implies 0.
- * See also, [[supportsRightContexts]].
- */
- maxRightContextCodeUnits?: number,
- }
-}
+```sh
+git ls-files | entr -c ./build.sh -tdd
```
+Importantly, `./build.sh -tdd` skips running the in-browser tests, and skips
+downloading/updating `npm` dependencies.
-### Message: `ready`
-
-Must be sent from the LMLayer to the keyboard when the LMLayer's model
-as a response to `initialize`. It will send `configuration`, which is
-a plain JavaScript object requesting configuration from the keyboard.
-
-There are only two options defined so far:
-
-```typescript
-interface ReadyMessage {
- message: 'ready';
- configuration: {
- /**
- * How many UTF-16 code units maximum to send as the context to the
- * left of the cursor ("left" in the Unicode character stream).
- *
- * Affects the `context` property sent in `predict` messages.
- *
- * While the left context MUST NOT bisect surrogate pairs, they MAY
- * bisect graphical clusters.
- */
- leftContextCodeUnits: number,
-
- /**
- * How many UTF-16 code units maximum to send as the context to the
- * right of the cursor ("right" in the Unicode character stream).
- *
- * Affects the `context` property sent in `predict` messages.
- *
- * While the left context MUST NOT bisect surrogate pairs, they MAY
- * bisect graphical clusters.
- */
- rightContextCodeUnits: number,
- };
-}
-```
-
-
-### Message: `predict`
-
-Sent from the keyboard to the LMLayer whenever a new prediction should
-be generated. This is typically initiated by a key press event. The
-keyboard **SHOULD** track each `predict` message using a [token][Tokens]. The
-token **MUST** be unique across all prediction events. The LMLayer
-**SHOULD** respond to each `predict` message with a `suggestions`
-message. The `suggestions` message **MUST** contain the corresponding
-token as sent in the initial `predict` message.
-
-The keyboard **MUST** send the `context` parameter. The keyboard
-**SHOULD** send the `transform` parameter. The keyboard **MUST** send
-a unique token.
-
-The semantics of the `predict` message **MUST** be from the
-perspective of this sequence of events:
-
- 1. After the input event is received by the keyboard.
- 2. Before the keyboard applies the associated `transform` to the buffer.
-
-**NOTE**: The keyboard **MAY** apply the `transform` associated with the
-input event before receiving the corresponding `suggestions` message
-from the LMLayer. The intention is that once the suggestions are displayed,
-the typist may select one of the suggestions in the place of the effects
-of their original input.
-
-**NOTE**: The keyboard **MAY** send the `predict` message after it has
-applied the `transform` associated with the input event to the buffer.
-Regardless of the actual sequence, the semantics **MUST** remain the
-same:the prediction happens from the perspective before the `transform`
-has been applied. Therefore, if the keyboard eagerly transforms the
-buffer before it has sent `predict` message, it must anticipate _undoing_
-the `transform` it has already applied if it is to apply a `transfrom`
-send in the `suggestions` message. The keyboard must act as if it has
-never applied the `transform` associated with the input event in the
-first place.
-
-The context is the text surrounding the insertion point, _before_ the
-transform is applied to the buffer.
-
-```typescript
-interface Context {
- /**
- * Up to maxLeftContextCodeUnits code units of Unicode scalar value
- * (i. e., characters) to the left of the insertion point in the
- * buffer. If there is nothing to the left of the buffer, this returns
- * an empty string.
- */
- left: USVString;
-
- /**
- * Up to maxRightContextCodeUnits code units of Unicode scalar value
- * (i. e., characters) to the right of the insertion point in the
- * buffer. If there is nothing to the right of the buffer, this returns
- * an empty string.
- */
- right?: USVString;
-
- /**
- * Whether the insertion point is at the start of the buffer.
- */
- startOfBuffer: boolean;
-
- /**
- * Whether the insertion point is at the end of the buffer.
- */
- endOfBuffer: boolean;
-}
-```
-
-The transform parameter describes how the input event will change the
-buffer.
-
-```typescript
-interface Transform {
- /**
- * The Unicode scalar values (i.e., characters) to be inserted at the
- * cursor position.
- *
- * Corresponds to `s` in com.keyman.KeyboardInterface.output.
- */
- insert: USVString;
-
- /**
- * The number of code units to delete to the left of the cursor.
- *
- * Corresponds to `dn` in com.keyman.KeyboardInterface.output.
- */
- delete: number;
-
- /**
- * The number of code units to delete to the right of the cursor.
- * Not available on all platforms.
- */
- deleteRight?: number;
-}
-```
-
-
-### Message: `suggestions`
-
-The `suggestions` message is sent from the LMLayer to the keyboard. This
-message sends a ranked array of suggestions, in descending order of
-probability (i.e., entry `0` is most likely, followed by entry `1`,
-etc.). This message **MUST** be in response to a `predict` message, and
-it **MUST** respond with the corresponding [token].
-
-```typescript
-/**
- * `suggestions` is an ordered array of suggestion objects.
- * Each suggestion is a transform bundled with a `displayAs` property.
- */
-let suggestions = Suggestion[];
-
-interface Suggestion {
- /**
- * Same object as an input event transform.
- * Note that the transform is applied AFTER the input event
- * transform.
- */
- transform: Transform;
- /**
- * A string to display the suggestion to the typist.
- * This should aid the typist understand what the transform
- * will do to their text.
- */
- displayAs: string;
-}
-```
-
-```javascript
-let suggestions = [
- {
- transform: {
- insert: 'teapot',
- deleteLeft: 1,
- deleteRight: 0
- },
- displayAs: '🍵'
- }
-];
-```
-
-
-#### Timing
-
-Each suggestion provides a `transform`. This transform is applied
-_after_ the transform associated with the input event that initiated
-this prediction. That is, the suggested transform applies to the buffer
-after the transform associated with the input event.
-
-Rephrased in somewhat mathematical terms:
-Let 𝑥 ∈ 𝐵 be the input text buffer. Let 𝑇𝑖 be the transform
-associated with an input event that maps a text buffer 𝐵 → 𝐵. Let
-𝑇𝑠 be a transform suggested through the LMLayer. 𝑦 ∈ 𝐵 is the
-text buffer after the suggestion transform has been applied. The
-correct sequence of applications should be as follows:
-
-> 𝑦 = 𝑇𝑠(𝑇𝑖(𝑥))
-
-
-#### Late suggestions
-
-Sometimes, a `suggestions` message may arrive after an input event has
-already invalidated its request. This is called a **"late" suggestion**.
-The LMLayer **MAY** send late suggestions. Consequently, the keyboard
-**MAY** discard the late suggestions. There is no requirement for the
-keyboard to acknowledge late suggestions, or for the LMLayer to avoid
-sending late `suggestions` messages. In either case, a `suggestions`
-message can be identified as appropriate or "late" via its `token`
-property.
-
-
-TODO
-====
-
- - [x] Document `suggestions`
- - [x] TypeScript!
- - [ ] make simple `index.html` that demos a dummy model
- - [x] Update class definitions in README from TypeScript sources.
- - [ ] Do word segmentation
- - [ ] Determine the exact arguments given to the model's `predict()`
- method.
- - [ ] Make an `error` initialization message.
- - [ ] Make a `cancel` message.
- - [ ] LOGLIKEIHOOD IN THE TRANSFORM!
- - [ ] `possibleTransforms` where `transform` is an alias for `possibleTransforms[0]`
- - [ ] Use [puppeteer](https://github.com/GoogleChrome/puppeteer)?
+[entr]: http://eradman.com/entrproject/
diff --git a/common/predictive-text/build.sh b/common/predictive-text/build.sh
index a3f55d303d..f48c658753 100755
--- a/common/predictive-text/build.sh
+++ b/common/predictive-text/build.sh
@@ -4,11 +4,15 @@
# Designed for optimal compatibility with the Keyman Suite.
#
+# Include some helper functions from resources
+. ../../resources/shellHelperFunctions.sh
+
LMLAYER_OUTPUT=build
WORKER_OUTPUT=build/intermediate
NAKED_WORKER=$WORKER_OUTPUT/index.js
EMBEDDED_WORKER=$WORKER_OUTPUT/embedded_worker.js
+
# Build the worker and the main script.
build ( ) {
# Build worker first; the main file depends on it.
@@ -53,21 +57,6 @@ display_usage ( ) {
echo " -test runs unit and integration tests after building"
}
-# Prints a nice, common error message.
-fail ( ) {
- # TODO: source shellHelperFunctions.sh
- local ERROR_RED
- local NORMAL
- ERROR_RED="$(tput setaf 1)"
- NORMAL="$(tput sgr0)"
- FAILURE_MSG="$1"
- if [[ "$FAILURE_MSG" == "" ]]; then
- FAILURE_MSG="Unknown failure."
- fi
- echo "$0: ${ERROR_RED}$FAILURE_MSG${NORMAL}"
- exit 1
-}
-
# Creates embedded_worker.js. Must be run after the worker is built for the
# first time
wrap-worker ( ) {
diff --git a/common/predictive-text/docs/worker-communication-protocol.md b/common/predictive-text/docs/worker-communication-protocol.md
new file mode 100644
index 0000000000..dd269e0dd0
--- /dev/null
+++ b/common/predictive-text/docs/worker-communication-protocol.md
@@ -0,0 +1,391 @@
+Keyman/Language Modelling layer
+================================
+
+This document introduces the protocol for communicating between
+KeymanWeb and the language modeling layer (i.e., the configurable
+prediction and suggestion engine, (henceforth referred to as the
+"LMLayer").
+
+Note: I'm using the term **keyboard** as a synonym for **KeymanWeb**.
+
+> The key words "MUST", "MUST NOT", "REQUIRED", "SHALL", "SHALL
+> NOT", "SHOULD", "SHOULD NOT", "RECOMMENDED", "MAY", and
+> "OPTIONAL" in this document are to be interpreted as described in
+> [RFC 2119].
+
+[RFC 2119]: https://www.ietf.org/rfc/rfc2119.txt
+
+
+Communication protocol between keyboard and asynchronous worker
+---------------------------------------------------------------
+
+
+
+We have decided that everything to the right of the `KeymanWeb` will be
+in a Web Worker. However, communication can happen only through
+[`postMessage(data)`][postMessage] commands,
+where `data` is a serializable object (via the [structured clone][]
+algorithm).
+
+What serializable object can we send that will adhere to the [open-closed principle]?
+
+### Messages
+
+The idea is to use a [discriminated union][]. The protocol involves
+plain JavaScript objects with one property called `message` that takes
+a finite set of `string` values.
+
+These string values indicate what message should be sent. The rest of
+the properties in the object are the parameters send with the message.
+
+```javascript
+{
+ message: 'predict',
+ // message-specific properties here
+}
+```
+
+See also: [XML-RPC][]
+
+Messages are **not** methods. That is, there is no assumption that
+a client will receive a reply when a message is sent. However, some
+pairs of messages, such as `predict` and `suggestions`, lightly assume
+request/response semantics.
+
+[discriminated union]: http://www.typescriptlang.org/docs/handbook/advanced-types.html#discriminated-unions
+[open-closed principle]: https://en.wikipedia.org/wiki/Open%E2%80%93closed_principle
+[XML-RPC]: https://en.wikipedia.org/wiki/XML-RPC
+[structured clone]: https://developer.mozilla.org/en-US/docs/Web/API/Web_Workers_API/Structured_clone_algorithm
+[postMessage]: https://developer.mozilla.org/en-US/docs/Web/API/Worker/postMessage
+
+
+### Tokens
+
+Tokens uniquely identify an input event, such as a key press. Since
+Keyman should ask for an asynchronous prediction on most key presses, the
+token is intended to associate a prediction and its response with
+a particular input; Keyman is free to ignore prediction responses if
+they are for outdated input events.
+
+The `Token` type is opaque to LMLayer. That is, LMLayer does not inspect
+its contents; it simply uses it to identify a request and pass it back
+to Keyman. There are a few requirements on the concrete type of the
+`Token`:
+
+ 1. Tokens **MUST** be serializable via the [structured clone][] algorithm;
+ 2. Tokens **MUST** be usable as a key in a [`Map`][Map object] object.
+ 3. Tokens **MUST** be unique across messages. That is, tokens **MUST
+ NOT** be duplicated between different messages.
+
+It is up to the keyboard to create unambiguous tokens that can be uniquely
+identified through the round-trip process.
+
+In the following examples, I'll use the subset of `number` values that
+are interpretable as [31-bit signed integers][Smi].
+
+[Map object]: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map#Key_equality
+[Smi]: https://github.com/thlorenz/v8-perf/blob/master/data-types.md#efficiently-representing-values-and-tagging
+[Tokens]: #tokens
+
+### Example
+
+An asynchronous message to predict after typing 'D':
+
+```javascript
+{
+ message: 'predict',
+
+ token: 1,
+ transform: {
+ insert: 'D',
+ deleteLeft: 0,
+ deleteRight: 0
+ },
+ context: {
+ left: '',
+ right: '',
+ startOfBuffer: true,
+ endOfBuffer: true
+ },
+}
+```
+
+Message types
+-------------
+
+Currently there are four message types:
+
+Message | Direction | Parameters | Expected reply | Uses token
+--------------|--------------------|---------------------|---------------------|---------------
+`initialize` | keyboard → LMLayer | initialization | Yes — `ready` | No
+`ready` | LMLayer → keyboard | configuration | No | No
+`predict` | keyboard → LMLayer | transform, context | Yes — `suggestions` | Yes
+`suggestions` | LMLayer → keyboard | suggestions | No | Yes
+
+
+### Message: `initialize`
+
+Must be sent from the keyboard to the LMLayer so that the LMLayer
+initializes a model. It will send `initialization` which is a plain
+JavaScript object specify the path to the model, as well configurations
+and platform restrictions.
+
+The keyboard **MUST NOT** send any messages to the LMLayer prior to
+sending `initialize`. The keyboard **SHOULD NOT** send another message
+to the keyboard until it receives `ready` message from the LMLayer
+before sending another message.
+
+These are the configurations, and platform restrictions sent to
+initialize the LMLayer and its model.
+
+```typescript
+interface InitializeMessage {
+ message: 'initialize';
+
+ /**
+ * Path to the model. There are no concrete restrictions on the path
+ * to the model, so long as the LMLayer can successfully use it to
+ * initialize the model.
+ */
+ model: string;
+
+ configuration: {
+ /**
+ * Whether the platform supports right contexts.
+ * The absence of this rule implies false.
+ */
+ supportsRightContexts?: false,
+
+ /**
+ * Whether the platform supports deleting to the right.
+ * The absence of this rule implies false.
+ */
+ supportsDeleteRight?: false,
+
+ /**
+ * The maximum amount of UTF-16 code units that the keyboard will
+ * provide to the left of the cursor, as an integer.
+ */
+ maxLeftContextCodeUnits: number,
+
+ /**
+ * The maximum amount of code units that the keyboard will provide to
+ * the right of the cursor, as an integer. The absence of this rule
+ * implies 0.
+ * See also, [[supportsRightContexts]].
+ */
+ maxRightContextCodeUnits?: number,
+ }
+}
+```
+
+
+### Message: `ready`
+
+Must be sent from the LMLayer to the keyboard when the LMLayer's model
+as a response to `initialize`. It will send `configuration`, which is
+a plain JavaScript object requesting configuration from the keyboard.
+
+There are only two options defined so far:
+
+```typescript
+interface ReadyMessage {
+ message: 'ready';
+ configuration: {
+ /**
+ * How many UTF-16 code units maximum to send as the context to the
+ * left of the cursor ("left" in the Unicode character stream).
+ *
+ * Affects the `context` property sent in `predict` messages.
+ *
+ * While the left context MUST NOT bisect surrogate pairs, they MAY
+ * bisect graphical clusters.
+ */
+ leftContextCodeUnits: number,
+
+ /**
+ * How many UTF-16 code units maximum to send as the context to the
+ * right of the cursor ("right" in the Unicode character stream).
+ *
+ * Affects the `context` property sent in `predict` messages.
+ *
+ * While the left context MUST NOT bisect surrogate pairs, they MAY
+ * bisect graphical clusters.
+ */
+ rightContextCodeUnits: number,
+ };
+}
+```
+
+
+### Message: `predict`
+
+Sent from the keyboard to the LMLayer whenever a new prediction should
+be generated. This is typically initiated by a key press event. The
+keyboard **SHOULD** track each `predict` message using a [token][Tokens]. The
+token **MUST** be unique across all prediction events. The LMLayer
+**SHOULD** respond to each `predict` message with a `suggestions`
+message. The `suggestions` message **MUST** contain the corresponding
+token as sent in the initial `predict` message.
+
+The keyboard **MUST** send the `context` parameter. The keyboard
+**SHOULD** send the `transform` parameter. The keyboard **MUST** send
+a unique token.
+
+The semantics of the `predict` message **MUST** be from the
+perspective of this sequence of events:
+
+ 1. After the input event is received by the keyboard.
+ 2. Before the keyboard applies the associated `transform` to the buffer.
+
+**NOTE**: The keyboard **MAY** apply the `transform` associated with the
+input event before receiving the corresponding `suggestions` message
+from the LMLayer. The intention is that once the suggestions are displayed,
+the typist may select one of the suggestions in the place of the effects
+of their original input.
+
+**NOTE**: The keyboard **MAY** send the `predict` message after it has
+applied the `transform` associated with the input event to the buffer.
+Regardless of the actual sequence, the semantics **MUST** remain the
+same:the prediction happens from the perspective before the `transform`
+has been applied. Therefore, if the keyboard eagerly transforms the
+buffer before it has sent `predict` message, it must anticipate _undoing_
+the `transform` it has already applied if it is to apply a `transfrom`
+send in the `suggestions` message. The keyboard must act as if it has
+never applied the `transform` associated with the input event in the
+first place.
+
+The context is the text surrounding the insertion point, _before_ the
+transform is applied to the buffer.
+
+```typescript
+interface Context {
+ /**
+ * Up to maxLeftContextCodeUnits code units of Unicode scalar value
+ * (i. e., characters) to the left of the insertion point in the
+ * buffer. If there is nothing to the left of the buffer, this returns
+ * an empty string.
+ */
+ left: USVString;
+
+ /**
+ * Up to maxRightContextCodeUnits code units of Unicode scalar value
+ * (i. e., characters) to the right of the insertion point in the
+ * buffer. If there is nothing to the right of the buffer, this returns
+ * an empty string.
+ */
+ right?: USVString;
+
+ /**
+ * Whether the insertion point is at the start of the buffer.
+ */
+ startOfBuffer: boolean;
+
+ /**
+ * Whether the insertion point is at the end of the buffer.
+ */
+ endOfBuffer: boolean;
+}
+```
+
+The transform parameter describes how the input event will change the
+buffer.
+
+```typescript
+interface Transform {
+ /**
+ * The Unicode scalar values (i.e., characters) to be inserted at the
+ * cursor position.
+ *
+ * Corresponds to `s` in com.keyman.KeyboardInterface.output.
+ */
+ insert: USVString;
+
+ /**
+ * The number of code units to delete to the left of the cursor.
+ *
+ * Corresponds to `dn` in com.keyman.KeyboardInterface.output.
+ */
+ delete: number;
+
+ /**
+ * The number of code units to delete to the right of the cursor.
+ * Not available on all platforms.
+ */
+ deleteRight?: number;
+}
+```
+
+
+### Message: `suggestions`
+
+The `suggestions` message is sent from the LMLayer to the keyboard. This
+message sends a ranked array of suggestions, in descending order of
+probability (i.e., entry `0` is most likely, followed by entry `1`,
+etc.). This message **MUST** be in response to a `predict` message, and
+it **MUST** respond with the corresponding [token].
+
+```typescript
+/**
+ * `suggestions` is an ordered array of suggestion objects.
+ * Each suggestion is a transform bundled with a `displayAs` property.
+ */
+let suggestions = Suggestion[];
+
+interface Suggestion {
+ /**
+ * Same object as an input event transform.
+ * Note that the transform is applied AFTER the input event
+ * transform.
+ */
+ transform: Transform;
+ /**
+ * A string to display the suggestion to the typist.
+ * This should aid the typist understand what the transform
+ * will do to their text.
+ */
+ displayAs: string;
+}
+```
+
+```javascript
+let suggestions = [
+ {
+ transform: {
+ insert: 'teapot',
+ deleteLeft: 1,
+ deleteRight: 0
+ },
+ displayAs: '🍵'
+ }
+];
+```
+
+
+#### Timing
+
+Each suggestion provides a `transform`. This transform is applied
+_after_ the transform associated with the input event that initiated
+this prediction. That is, the suggested transform applies to the buffer
+after the transform associated with the input event.
+
+Rephrased in somewhat mathematical terms:
+Let 𝑥 ∈ 𝐵 be the input text buffer. Let 𝑇𝑖 be the transform
+associated with an input event that maps a text buffer 𝐵 → 𝐵. Let
+𝑇𝑠 be a transform suggested through the LMLayer. 𝑦 ∈ 𝐵 is the
+text buffer after the suggestion transform has been applied. The
+correct sequence of applications should be as follows:
+
+> 𝑦 = 𝑇𝑠(𝑇𝑖(𝑥))
+
+
+#### Late suggestions
+
+Sometimes, a `suggestions` message may arrive after an input event has
+already invalidated its request. This is called a **"late" suggestion**.
+The LMLayer **MAY** send late suggestions. Consequently, the keyboard
+**MAY** discard the late suggestions. There is no requirement for the
+keyboard to acknowledge late suggestions, or for the LMLayer to avoid
+sending late `suggestions` messages. In either case, a `suggestions`
+message can be identified as appropriate or "late" via its `token`
+property.
diff --git a/common/predictive-text/embedded_worker.d.ts b/common/predictive-text/embedded_worker.d.ts
index 30f2575232..d7df1885d8 100644
--- a/common/predictive-text/embedded_worker.d.ts
+++ b/common/predictive-text/embedded_worker.d.ts
@@ -21,7 +21,9 @@
*/
// Include the code intended to run WITHIN the Web Worker.
-// The worker code must be compiled before this file is compiled.
+// The worker code MUST be compiled before this file is compiled.
+// If you see a 'File: embedded_worker.js not found.' error message, please
+// compile the worker first (stage one).
///
/**
diff --git a/common/predictive-text/index.ts b/common/predictive-text/index.ts
index 02cc9c2d79..1af4ab6871 100644
--- a/common/predictive-text/index.ts
+++ b/common/predictive-text/index.ts
@@ -32,20 +32,55 @@
*/
type USVString = string;
-// TODO: document
+/**
+ * Top-level interface to the Language Modelling layer, or "LMLayer" for short.
+ *
+ * The Language Modelling layer provides a way for keyboards to offer prediction and
+ * correction functionalities. The LMLayer proper runs within a Web Worker, however,
+ * this class is intended to run in the main thread, and automatically spawn a Web
+ * Worker, capable of offering predictions.
+ *
+ * Since the Worker runs in a different thread, the public methods of this class are
+ * asynchronous. Methods of note include:
+ *
+ * - #initialize() -- initialize the LMLayer with a configuration and language model
+ * - #predict() -- ask the LMLayer to offer suggestions (predictions or corrections) for
+ * the input event
+ *
+ * The top-level LMLayer will automatically starts up its own Web Worker.
+ */
class LMLayer {
/**
* The underlying worker instance. By default, this is the LMLayerWorker.
*/
private _worker: Worker;
+ /**
+ * Construct the top-level LMLayer interface. This also starts the underlying Worker.
+ * Make sure to call .initialize() when using the default Worker.
+ *
+ * @param uri URI of the underlying LMLayer worker code. This will usually be a blob:
+ * or file: URI. If uri is not provided, this will start the default Worker.
+ */
constructor(uri?: string) {
this._worker = new Worker(uri || LMLayer.asBlobURI(LMLayerWorkerCode));
}
+ // TODO: asynchronous initialize() method, based on
+ // https://github.com/eddieantonio/keyman-lmlayer-prototype/blob/f8e6268b03190d08cf5d35f9428cf9150d6d219e/index.ts#L42-L62
+
+ // TODO: asynchronous predict() method, based on
+ // https://github.com/eddieantonio/keyman-lmlayer-prototype/blob/f8e6268b03190d08cf5d35f9428cf9150d6d219e/index.ts#L64-L80
+
+ // TODO: asynchronous close() method.
+ // Worker code must recognize message and call self.close().
+
/**
- * Given a function, this utility returns the source code within it.
- * @param fn
+ * Given a function, this utility returns the source code within it, as a string.
+ * This is intended to unwrap the "wrapped" source code created in the LMLayerWorker
+ * build process.
+ *
+ * @param fn The function whose body will be returned.
*/
static unwrap(fn: Function): string {
let wrapper = fn.toString();
@@ -74,7 +109,7 @@ class LMLayer {
}
}
-// Let LMLayer be available both in browser and in Node.
+// Let LMLayer be available both in the browser and in Node.
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = LMLayer;
} else {
diff --git a/common/predictive-text/models/en-x-test-teapot.js b/common/predictive-text/models/en-x-test-teapot.js
deleted file mode 100644
index ac225e89f4..0000000000
--- a/common/predictive-text/models/en-x-test-teapot.js
+++ /dev/null
@@ -1,55 +0,0 @@
-/*
- * Copyright (c) 2018 National Research Council Canada (author: Eddie A. Santos)
- * Copyright (c) 2018 SIL International
- *
- * Permission is hereby granted, free of charge, to any person obtaining a copy of
- * this software and associated documentation files (the "Software"), to deal in
- * the Software without restriction, including without limitation the rights to
- * use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
- * the Software, and to permit persons to whom the Software is furnished to do so,
- * subject to the following conditions:
- *
- * The above copyright notice and this permission notice shall be included in all
- * copies or substantial portions of the Software.
- *
- * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
- * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
- * FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
- * COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
- * IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
- * CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
- */
-
-/**
- * This is a test model that simply returns "teapot" when given the context of
- * «I'm a little ».
- */
-registerModel(function () {
- return {
- predict(context, transform) {
- return [
- {
- transform: {
- insert: 'teapot',
- delete: transform.insert.length,
- deleteRight: 0,
- },
- displayAs: '🍵',
- weight: 0.00,
- }
- ];
-
- /* TODO:
- if (context.wordsLeft === ["I'm", "a", "little"] &&
- transform.insert === 't') {
- }
-
- return [];
- */
- },
-
- configuration: {
- }
- };
-});
-/*global registerModel*/
diff --git a/common/predictive-text/unit_tests/headless/lmlayer.js b/common/predictive-text/unit_tests/headless/top-level-lmlayer.js
similarity index 55%
rename from common/predictive-text/unit_tests/headless/lmlayer.js
rename to common/predictive-text/unit_tests/headless/top-level-lmlayer.js
index e13ec29838..645395f4cf 100644
--- a/common/predictive-text/unit_tests/headless/lmlayer.js
+++ b/common/predictive-text/unit_tests/headless/top-level-lmlayer.js
@@ -1,8 +1,10 @@
var assert = require('chai').assert;
var sinon = require('sinon');
-let LMLayer = require('../../build/');
+let LMLayer = require('../../build');
+// Test the top-level LMLayer interface.
+// Note: these tests can only be run after BOTH stages of compilation are completed.
describe('LMLayer', function() {
describe('[[constructor]]', function () {
it.skip('should be take a URI to instantiate', function () {
@@ -10,17 +12,16 @@ describe('LMLayer', function() {
});
});
- /**
- * .unwrap() is a static function that unwraps some function code.
- */
+ // Since the Blob API is browser-specific, look for those tests
+ // for .asBlobURI() in the in_browser tests.
describe('.unwrap', function () {
- // Since the Blob API is DOM-specific, look for those tests
- // in the in_browser tests.
it('should return the inner code of a function', function () {
+ // Create a multi-line function body we can match in a RegExp.
let text = LMLayer.unwrap(function hello() {
var hello;
var world;
});
+ // Unwrap should give us back ONLY the body. Whitespace isn't really important.
assert.match(text, /^\s*var\s+hello;\s*var\s+world;\s*$/);
});
});
diff --git a/common/predictive-text/unit_tests/headless/worker-initialization.js b/common/predictive-text/unit_tests/headless/worker-initialization.js
index de2ad0cefc..d543de6be1 100644
--- a/common/predictive-text/unit_tests/headless/worker-initialization.js
+++ b/common/predictive-text/unit_tests/headless/worker-initialization.js
@@ -3,8 +3,12 @@ var sinon = require('sinon');
let LMLayerWorker = require('../../build/intermediate');
+// Unit tests for instantiating and initializing the LMLayer Worker in isolation.
+//
+// Although the LMLayerWorker expected to be used inside a DedicatedWorkerGlobalScope,
+// these unit tests DO NOT run inside a Worker, and instead use Sinon fakes to assert
+// behavior.
describe('LMLayerWorker', function() {
-
describe('#constructor()', function() {
it('should allow for the mocking of postMessage()', function () {
var fakePostMessage = sinon.fake();
@@ -147,6 +151,7 @@ describe('LMLayerWorker', function() {
});
});
+ // TODO: move these tests to a different file.
describe('Message: predict', function () {
it.skip('should predict from a local model', function () {
// will need import scripts figured out
@@ -161,6 +166,11 @@ describe('LMLayerWorker', function() {
return { data };
}
+ /**
+ * Deprecation warning: Soon, models will not be requierd to be passed as source code;
+ * when this happens, tests should refrain from sending source code for the model
+ * parameter.
+ */
function dummyModelCode() {
return 'return {model: {}, configuration: {}}';
}
diff --git a/common/predictive-text/unit_tests/in_browser/cases/lmlayer-integration.js b/common/predictive-text/unit_tests/in_browser/cases/top-level-lmlayer.js
similarity index 66%
rename from common/predictive-text/unit_tests/in_browser/cases/lmlayer-integration.js
rename to common/predictive-text/unit_tests/in_browser/cases/top-level-lmlayer.js
index a37bfed7a4..f995fc1d20 100644
--- a/common/predictive-text/unit_tests/in_browser/cases/lmlayer-integration.js
+++ b/common/predictive-text/unit_tests/in_browser/cases/top-level-lmlayer.js
@@ -8,12 +8,15 @@ describe('LMLayer', function () {
});
});
- describe('#asBlobURI', function () {
+ describe('#asBlobURI()', function () {
+ // #asBlobURI() requires browser APIs, hence why it cannot be tested headless in Node.
it('should take a function and convert it into a blob function', function (done) {
let uri = LMLayer.asBlobURI(function dummyHandler() {
- // Post something weird, so we can be reasonably certain it's not a fluke.
- // WARNING: Do NOT factor out this string as a variable.
- // It MUST remain a string in this function body, because it gets stringified!
+ // Post something weird, so we can be reasonably certain the Web Worker is...
+ // well, working.
+ // WARNING: Do NOT refactor this string as a variable. It **MUST** remain a string
+ // in this function body, because the code in this function's body gets
+ // stringified!
postMessage('fhqwhgads');
});
assert.match(uri, /^blob:/);
diff --git a/common/predictive-text/unit_tests/in_browser/cases/basic.js b/common/predictive-text/unit_tests/in_browser/cases/worker.js
similarity index 100%
rename from common/predictive-text/unit_tests/in_browser/cases/basic.js
rename to common/predictive-text/unit_tests/in_browser/cases/worker.js
diff --git a/common/predictive-text/worker/index.ts b/common/predictive-text/worker/index.ts
index 55473f4c14..33ca33151f 100644
--- a/common/predictive-text/worker/index.ts
+++ b/common/predictive-text/worker/index.ts
@@ -20,33 +20,37 @@
* CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
*/
+/**
+ * The signature of self.postMessage(), so that unit tests can mock it.
+ */
type PostMessage = typeof DedicatedWorkerGlobalScope.prototype.postMessage;
+
+/**
+ * The valid outgoing message types.
+ */
type OutgoingMessageKind = 'ready' | 'suggestions';
+/**
+ * The structure of an initialization message. It should include the model (either in
+ * source code or parameter form), as well as the keyboard's capabilities.
+ */
interface InitializeMessage {
/**
- * Source code of the model.
- * TODO: write a description of what this source code should look like.
+ * The model, and its configuration.
+ * TODO: write a description of what this actually is!
*/
- model: string;
- /**
- * The configuration that the keyboard can offer to the model.
- */
- configuration: RequestedConfiguration;
-}
-
-interface InitializeMessage {
- /**
- * Source code of the model.
- * TODO: write a description of what this source code should look like.
- */
- model: string;
+ model: any;
+
/**
* The configuration that the keyboard can offer to the model.
*/
+ // TODO: rename to capabilities? They **are** the capabilities of the keyboard's platform.
configuration: RequestedConfiguration;
}
+/**
+ * The structure of the message back to the keyboard.
+ */
interface ReadyMessage {
configuration: ModelConfiguration;
}
@@ -104,7 +108,7 @@ interface ModelConfiguration {
rightContextCodeUnits: number;
};
-// TODO:
+// TODO: define what valid values of the model are.
interface Model {};
/**
@@ -222,7 +226,7 @@ class LMLayerWorker {
}
}
-// Let LMLayerWorker be available both in browser and in Node.
+// Let LMLayerWorker be available both in the browser and in Node.
if (typeof module !== 'undefined' && typeof module.exports !== 'undefined') {
module.exports = LMLayerWorker;
} else if (typeof self !== 'undefined' && 'postMessage' in self) {