Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,7 @@ enum TimedTaskTypes {
CORRECTING = 2
}

enum PathEdge {
export enum PathEdge {
ROOT = 'root',
INSERTION = 'insertion',
DELETION = 'deletion',
Expand Down Expand Up @@ -162,7 +162,7 @@ export class SearchNode {
* Notes the edit operation used for the most recent edge in the node's
* represented search path.
*/
private readonly lastEdgeType: PathEdge;
readonly lastEdgeType: PathEdge;

constructor(rootTraversal: LexiconTraversal, spaceId: number, toKey?: (arg0: string) => string);
constructor(node: SearchNode, spaceId?: number, edgeType?: PathEdge);
Expand Down Expand Up @@ -276,7 +276,11 @@ export class SearchNode {
* character not seen in the input, as if the user accidentally skipped typing
* it. No new input will be expected, but the search will continue one
* character deeper in the backing lexicon.
* @param spaceId
* @param spaceId A unique identifier associated with the SearchQuotientNode
* that calls this method and processes the resulting SearchNodes.
*
* If left empty, the nodes will be associated with the same SearchQuotientNode
* as this instance.
* @returns An array of SearchNodes corresponding to lexical entries that are
* prefixed with the lexicon entry represented by the current Node's
* matchSequence text.
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
/**
* Keyman is copyright (C) SIL Global. MIT License.
*
* Created by jahorton on 2026-02-03
*
* This file adds a SearchQuotientSpur variant modeling insertion of
* lexical-entry prefix characters - an operation with no corresponding
* keystroke.
*/

import { SENTINEL_CODE_UNIT } from "@keymanapp/models-templates";
import { SearchNode } from "./distance-modeler.js";
import { SearchQuotientNode } from "./search-quotient-node.js";
import { SearchQuotientSpur } from "./search-quotient-spur.js";
import { TokenResultMapping } from "./token-result-mapping.js";

export class InsertionQuotientSpur extends SearchQuotientSpur {
public readonly insertLength = 1;
public readonly leftDeleteLength = 0;

constructor(
parentNode: SearchQuotientNode
) {
super(parentNode, null, null, parentNode.codepointLength + 1);
}

construct(parentNode: SearchQuotientNode): this {
return new InsertionQuotientSpur(parentNode) as this;
}

protected buildEdgesFromResults(baseNodes: ReadonlyArray<TokenResultMapping>): SearchNode[] {
// Note that .buildInsertionEdges will not extend any nodes reached by empty-input
// or by deletions.
return baseNodes
// If there are already at least 2 edits for a node, do not add new edits.
// Also, do not permit insert edits to follow delete edits.
.filter((n) => n.lastEdgeType != 'deletion' && n.editCount < 2)
.flatMap((n) => n.buildInsertionEdges(this.spaceId));
}

get edgeKey(): string {
return `SR[${this.parentNode.sourceRangeKey}]L${this.codepointLength}INS`;
}

get bestExample() {
const base = this.parentNode.bestExample;
// We use the SENTINEL char as an insertion place-holder, as there's no
// actual keystroke to source better characters from.
base.text += SENTINEL_CODE_UNIT;

return base;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -181,16 +181,16 @@ export class SearchQuotientCluster extends SearchQuotientNode {

// What if we're trying to merge something previously split?
// That can only happen at the head of the incoming space, so we check for it early here.
if(space.inputCount == 1 && space instanceof SearchQuotientSpur) {
if(space.inputCount == 1 && space instanceof SearchQuotientSpur && space.parents[0] instanceof SearchQuotientRoot) {
// In such a case... the 'leading edge' of the incoming space needs to be checked
// against the trailing edge of `this` instance's entries.
const thisTailInputSource = this.inputSegments[this.inputSegments.length - 1];
const thisTailSpaceIds = this.parents.map((path) => (path as SearchQuotientSpur).inputSource.subsetId);
const thisTailSpaceIds = this.parents.map((path) => (path as SearchQuotientSpur).inputSource?.subsetId);
const spaceHeadInputSource = space.inputSegments[0];

const isOnSplitInput =
thisTailSpaceIds.some((entry) => entry == space.inputSource.subsetId)
&& thisTailInputSource.end == spaceHeadInputSource.start;
&& thisTailInputSource?.end == spaceHeadInputSource.start;

// In this case, we only rebuild the single path; an outer stack frame will reconstitute
// the split cluster from the individual paths built here.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -130,8 +130,8 @@ export class TokenResultMapping implements CorrectionResultMapping<SearchNode>{
return new SearchNode(this.node, spaceId);
}

buildInsertionEdges(): SearchNode[] {
return this.node.buildInsertionEdges();
buildInsertionEdges(spaceId?: number): SearchNode[] {
return this.node.buildInsertionEdges(spaceId);
}

buildDeletionEdges(dist: Distribution<Transform>, edgeId: number): SearchNode[] {
Expand All @@ -141,4 +141,8 @@ export class TokenResultMapping implements CorrectionResultMapping<SearchNode>{
buildSubstitutionEdges(dist: Distribution<Transform>, edgeId: number): SearchNode[] {
return this.node.buildSubstitutionEdges(dist, edgeId);
}

get lastEdgeType() {
return this.node.lastEdgeType;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export { ContextTransition } from './correction/context-transition.js';
export * from './correction/correction-searchable.js';
export * from './correction/correction-result-mapping.js';
export * from './correction/distance-modeler.js';
export * from './correction/insertion-quotient-spur.js';
export * from './correction/substitution-quotient-spur.js';
export * from './correction/search-quotient-cluster.js';
export * from './correction/search-quotient-spur.js';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,10 @@ describe('buildQuotientDocFixture() fixture', () => {
it('constructs paths properly', () => {
const {searchRoot, nodes} = buildQuotientDocFixture();

[searchRoot /*, nodes.sc1, nodes.sc2*/].forEach((n) => {
[searchRoot, nodes.sc1, nodes.sc2].forEach((n) => {
assert.equal(n.inputCount, 0);
});
[/*nodes.k1c0,*/ nodes.k1c1, nodes.k1c2 /*, nodes.k1c3*/].forEach((n) => {
[/*nodes.k1c0,*/ nodes.k1c1, nodes.k1c2, nodes.k1c3].forEach((n) => {
assert.equal(n.inputCount, 1);
});
[/*nodes.k2c0, nodes.k2c1,*/ nodes.k2c2, nodes.k2c3].forEach((n) => {
Expand All @@ -19,13 +19,13 @@ describe('buildQuotientDocFixture() fixture', () => {
[searchRoot/*, nodes.k1c0, nodes.k2c0*/].forEach((n) => {
assert.equal(n.codepointLength, 0);
});
[/*nodes.sc1, */nodes.k1c1/*, nodes.k2c1*/].forEach((n) => {
[nodes.sc1, nodes.k1c1/*, nodes.k2c1*/].forEach((n) => {
assert.equal(n.codepointLength, 1);
});
[/*nodes.sc2,*/ nodes.k1c2, nodes.k2c2].forEach((n) => {
[nodes.sc2, nodes.k1c2, nodes.k2c2].forEach((n) => {
assert.equal(n.codepointLength, 2);
});
[/*nodes.k1c3,*/ nodes.k2c3].forEach((n) => {
[nodes.k1c3, nodes.k2c3].forEach((n) => {
assert.equal(n.codepointLength, 3);
});
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs'

import {
generateSubsetId,
InsertionQuotientSpur,
models,
SearchQuotientCluster,
SearchQuotientRoot,
Expand Down Expand Up @@ -48,8 +49,8 @@ export function buildQuotientDocFixture() {
{ sample: { insert: 'cd', deleteLeft: 0, id: key1Id }, p: .2 }
];

// const sc1 = new InsertionQuotientSpur(searchRoot);
// const sc2 = new InsertionQuotientSpur(sc1);
const sc1 = new InsertionQuotientSpur(searchRoot);
const sc2 = new InsertionQuotientSpur(sc1);

// // K1C0
// const k1c0 = new DeletionQuotientSpur(searchRoot, abDistrib.concat(cdDistrib), {
Expand Down Expand Up @@ -93,15 +94,15 @@ export function buildQuotientDocFixture() {
// subsetId: generateSubsetId(),
// bestProbFromSet: abDistrib[0].p
// });
// const k1c2_ab = new SubstitutionQuotientSpur(sc1, abDistrib, {
// segment: {
// transitionId: key1Id,
// start: 0
// },
// // Deletions always get their own unique subset ID.
// subsetId: generateSubsetId(),
// bestProbFromSet: abDistrib[0].p
// });
const k1c2_ab = new SubstitutionQuotientSpur(sc1, abDistrib, {
segment: {
transitionId: key1Id,
start: 0
},
// Deletions always get their own unique subset ID.
subsetId: generateSubsetId(),
bestProbFromSet: abDistrib[0].p
});
const k1c2_cd = new SubstitutionQuotientSpur(searchRoot, cdDistrib, {
segment: {
transitionId: key1Id,
Expand All @@ -111,29 +112,29 @@ export function buildQuotientDocFixture() {
subsetId: generateSubsetId(),
bestProbFromSet: abDistrib[0].p
});
// const k1c2_ins = new InsertionQuotientSpur(k1c1);
const k1c2 = new SearchQuotientCluster([/*k1c2_del, k1c2_ab, */ k1c2_cd, /*k1c2_ins*/]);
const k1c2_ins = new InsertionQuotientSpur(k1c1);
const k1c2 = new SearchQuotientCluster([/*k1c2_del, */ k1c2_ab, k1c2_cd, k1c2_ins]);

// const k1c3_ab = new SubstitutionQuotientSpur(sc2, abDistrib, {
// segment: {
// transitionId: key1Id,
// start: 0
// },
// // Deletions always get their own unique subset ID.
// subsetId: generateSubsetId(),
// bestProbFromSet: abDistrib[0].p
// });
// const k1c3_cd = new SubstitutionQuotientSpur(sc1, cdDistrib, {
// segment: {
// transitionId: key1Id,
// start: 0
// },
// // Deletions always get their own unique subset ID.
// subsetId: generateSubsetId(),
// bestProbFromSet: abDistrib[0].p
// });
// const k1c3_ins = new InsertionQuotientSpur(k1c2);
// const k1c3 = new SearchQuotientCluster([k1c3_ab, k1c3_cd, k1c3_ins]);
const k1c3_ab = new SubstitutionQuotientSpur(sc2, abDistrib, {
segment: {
transitionId: key1Id,
start: 0
},
// Deletions always get their own unique subset ID.
subsetId: generateSubsetId(),
bestProbFromSet: abDistrib[0].p
});
const k1c3_cd = new SubstitutionQuotientSpur(sc1, cdDistrib, {
segment: {
transitionId: key1Id,
start: 0
},
// Deletions always get their own unique subset ID.
subsetId: generateSubsetId(),
bestProbFromSet: abDistrib[0].p
});
const k1c3_ins = new InsertionQuotientSpur(k1c2);
const k1c3 = new SearchQuotientCluster([k1c3_ab, k1c3_cd, k1c3_ins]);

// Onto keystroke 2.

Expand Down Expand Up @@ -235,12 +236,12 @@ export function buildQuotientDocFixture() {
subsetId: generateSubsetId(),
bestProbFromSet: efDistrib[0].p
});
// const k2c3_ins = new InsertionQuotientSpur(k2c2);
const k2c3 = new SearchQuotientCluster([/*k2c3_del, */ k2c3_ef, k2c3_gh, /*k2c3_ins*/]);
const k2c3_ins = new InsertionQuotientSpur(k2c2);
const k2c3 = new SearchQuotientCluster([/*k2c3_del, */ k2c3_ef, k2c3_gh, k2c3_ins]);

return {
searchRoot,
spurs: {/*sc1, sc2,*/ k1c1_ab, /*k1c2_ab,*/ k1c2_cd, /*k1c2_ins, k1c3_ab, k1c3_cd, k1c3_ins,*/ k2c2_ef, k2c3_ef, k2c3_gh /*, k2c3_ins*/},
nodes: {/* sc1, sc2, k1c0, */ k1c1, k1c2, /* k1c3, k2c0, k2c1, */ k2c2, k2c3}
spurs: {sc1, sc2, k1c1_ab, k1c2_ab, k1c2_cd, k1c2_ins, k1c3_ab, k1c3_cd, k1c3_ins, k2c2_ef, k2c3_ef, k2c3_gh, k2c3_ins},
nodes: {sc1, sc2, /* k1c0, */ k1c1, k1c2, k1c3, /* k2c0, k2c1, */ k2c2, k2c3}
};
}
Loading
Loading