mirror of
https://github.com/keymanapp/keyman.git
synced 2026-08-31 20:57:41 +00:00
Merge pull request #14840 from keymanapp/feat/web/split-context-tokens
feat(web): add class method for splitting ContextToken instances 🚂
This commit is contained in:
commit
da444878ff
4 changed files with 460 additions and 4 deletions
|
|
@ -7,10 +7,12 @@
|
|||
* in the context and associated correction-search progress and results.
|
||||
*/
|
||||
|
||||
import { buildMergedTransform } from "@keymanapp/models-templates";
|
||||
import { applyTransform, buildMergedTransform } from "@keymanapp/models-templates";
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
import { deepCopy, KMWString } from "@keymanapp/web-utils";
|
||||
|
||||
import { SearchSpace } from "./distance-modeler.js";
|
||||
import { TokenSplitMap } from "./context-tokenization.js";
|
||||
|
||||
import Distribution = LexicalModelTypes.Distribution;
|
||||
import LexicalModel = LexicalModelTypes.LexicalModel;
|
||||
|
|
@ -206,4 +208,126 @@ export class ContextToken {
|
|||
const composite = transforms.reduce((accum, current) => buildMergedTransform(accum, current), {insert: '', deleteLeft: 0});
|
||||
return composite.insert;
|
||||
}
|
||||
|
||||
/**
|
||||
* Splits this token into multiple tokens as defined by a `TokenSplitMap`.
|
||||
* @param split
|
||||
* @param lexicalModel
|
||||
* @returns
|
||||
*/
|
||||
split(split: TokenSplitMap, lexicalModel: LexicalModel) {
|
||||
const tokensFromSplit: ContextToken[] = [];
|
||||
|
||||
// Build an alternate version of the transforms: if we preprocess all deleteLefts,
|
||||
// what text remains from each?
|
||||
const alteredSources = preprocessInputSources(this.inputRange);
|
||||
|
||||
const blankContext = { left: '', startOfBuffer: true, endOfBuffer: true };
|
||||
const splitSpecs = split.matches.slice();
|
||||
let currentText = {...blankContext};
|
||||
let lenBeforeLastApply = 0;
|
||||
let committedLen = 0;
|
||||
let constructingToken = new ContextToken(lexicalModel);
|
||||
let backupToken: ContextToken;
|
||||
let transformIndex = 0;
|
||||
while(splitSpecs.length > 0) {
|
||||
const splitMatch = splitSpecs[0];
|
||||
|
||||
if(splitMatch.text == currentText.left) {
|
||||
tokensFromSplit.push(constructingToken);
|
||||
constructingToken = new ContextToken(lexicalModel);
|
||||
backupToken = null;
|
||||
committedLen += lenBeforeLastApply;
|
||||
currentText = {...blankContext};
|
||||
splitSpecs.shift();
|
||||
continue;
|
||||
} else if(currentText.left.indexOf(splitMatch.text) > -1) {
|
||||
// Oh dear - we've overshot the target! The split is awkward, in the
|
||||
// middle of a keystroke.
|
||||
|
||||
// Restore!
|
||||
const overextendedToken = constructingToken;
|
||||
constructingToken = backupToken;
|
||||
|
||||
// We know how much of the next transform to pull in: it's specified on
|
||||
// the split object. Excess on constructed token - the split 'text offset'
|
||||
const totalLenBeforeLastApply = committedLen + lenBeforeLastApply;
|
||||
// We read the start position for the NEXT token to know the split position.
|
||||
const extraCharsAdded = splitSpecs[1].textOffset - totalLenBeforeLastApply;
|
||||
const tokenSequence = overextendedToken.searchSpace.inputSequence;
|
||||
const lastInputIndex = tokenSequence.length - 1;
|
||||
const inputDistribution = tokenSequence[lastInputIndex];
|
||||
const headDistribution = inputDistribution.map((m) => {
|
||||
return {
|
||||
sample: {
|
||||
...m.sample,
|
||||
insert: KMWString.substring(m.sample.insert, 0, extraCharsAdded),
|
||||
deleteRight: 0
|
||||
}, p: m.p
|
||||
};
|
||||
});
|
||||
const tailDistribution = inputDistribution.map((m) => {
|
||||
return {
|
||||
sample: {
|
||||
...m.sample,
|
||||
insert: KMWString.substring(m.sample.insert, extraCharsAdded),
|
||||
deleteLeft: 0
|
||||
}, p: m.p
|
||||
};
|
||||
});
|
||||
|
||||
const priorSourceInput = overextendedToken.inputRange[lastInputIndex];
|
||||
constructingToken.addInput(priorSourceInput, headDistribution);
|
||||
tokensFromSplit.push(constructingToken);
|
||||
|
||||
constructingToken = new ContextToken(lexicalModel);
|
||||
backupToken = new ContextToken(constructingToken);
|
||||
constructingToken.addInput({
|
||||
trueTransform: priorSourceInput.trueTransform,
|
||||
inputStartIndex: priorSourceInput.inputStartIndex + extraCharsAdded
|
||||
}, tailDistribution);
|
||||
|
||||
const lenToCommit = lenBeforeLastApply + extraCharsAdded;
|
||||
splitSpecs.shift();
|
||||
|
||||
committedLen += lenToCommit;
|
||||
currentText.left = KMWString.substring(currentText.left, lenToCommit);
|
||||
lenBeforeLastApply = 0;
|
||||
continue; // without incrementing transformIndex - we haven't processed a new one!
|
||||
} else if(transformIndex == alteredSources.length) {
|
||||
throw new Error("Invalid split specified!");
|
||||
}
|
||||
|
||||
backupToken = new ContextToken(constructingToken);
|
||||
lenBeforeLastApply = KMWString.length(currentText.left);
|
||||
currentText = applyTransform(alteredSources[transformIndex].trueTransform, currentText);
|
||||
constructingToken.addInput(this.inputRange[transformIndex], this.searchSpace.inputSequence[transformIndex]);
|
||||
transformIndex++;
|
||||
}
|
||||
|
||||
return tokensFromSplit;
|
||||
}
|
||||
}
|
||||
|
||||
export function preprocessInputSources(inputSources: ReadonlyArray<TokenInputSource>) {
|
||||
const alteredSources = deepCopy(inputSources);
|
||||
let trickledDeleteLeft = 0;
|
||||
for(let i = alteredSources.length - 1; i >= 0; i--) {
|
||||
const source = alteredSources[i];
|
||||
if(trickledDeleteLeft) {
|
||||
const insLen = KMWString.length(source.trueTransform.insert);
|
||||
if(insLen <= trickledDeleteLeft) {
|
||||
source.trueTransform.insert = '';
|
||||
trickledDeleteLeft -= insLen;
|
||||
} else {
|
||||
source.trueTransform.insert = KMWString.substring(source.trueTransform.insert, 0, insLen - trickledDeleteLeft);
|
||||
trickledDeleteLeft = 0;
|
||||
}
|
||||
}
|
||||
trickledDeleteLeft += source.trueTransform.deleteLeft;
|
||||
source.trueTransform.deleteLeft = 0;
|
||||
}
|
||||
|
||||
alteredSources[0].trueTransform.deleteLeft = trickledDeleteLeft;
|
||||
return alteredSources;
|
||||
}
|
||||
|
|
@ -38,7 +38,7 @@ interface TokenMergeMap {
|
|||
match: EditTokenMap
|
||||
};
|
||||
|
||||
interface TokenSplitMap {
|
||||
export interface TokenSplitMap {
|
||||
input: EditTokenMap,
|
||||
matches: (EditTokenMap & { textOffset: number })[]
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
export { ClassicalDistanceCalculation, EditOperation, EditTuple, forNewIndices } from './correction/classical-calculation.js';
|
||||
export * from './correction/context-state.js';
|
||||
export { ContextToken } from './correction/context-token.js';
|
||||
export * from './correction/context-token.js';
|
||||
export * from './correction/context-tokenization.js';
|
||||
export { ContextTracker } from './correction/context-tracker.js';
|
||||
export { ContextTransition } from './correction/context-transition.js';
|
||||
|
|
|
|||
|
|
@ -12,16 +12,44 @@ import { assert } from 'chai';
|
|||
// Aliased due to JS keyword.
|
||||
import { default as defaultBreaker } from '@keymanapp/models-wordbreakers';
|
||||
import { jsonFixture } from '@keymanapp/common-test-resources/model-helpers.mjs';
|
||||
import { LexicalModelTypes } from '@keymanapp/common-types';
|
||||
|
||||
import { ContextToken, correction, models } from '@keymanapp/lm-worker/test-index';
|
||||
import { ContextToken, correction, models, preprocessInputSources } from '@keymanapp/lm-worker/test-index';
|
||||
|
||||
import Distribution = LexicalModelTypes.Distribution;
|
||||
import ExecutionTimer = correction.ExecutionTimer;
|
||||
import Transform = LexicalModelTypes.Transform;
|
||||
import TrieModel = models.TrieModel;
|
||||
import { KMWString } from '@keymanapp/web-utils';
|
||||
|
||||
var plainModel = new TrieModel(jsonFixture('models/tries/english-1000'),
|
||||
{wordBreaker: defaultBreaker});
|
||||
|
||||
// https://www.compart.com/en/unicode/block/U+1D400
|
||||
const mathBoldUpperA = 0x1D400; // Mathematical Bold Capital A
|
||||
const mathBoldLowerA = 0x1D41A; // Small A
|
||||
|
||||
function toMathematicalSMP(text: string) {
|
||||
const chars = [...text];
|
||||
|
||||
const asSMP = chars.map((c) => {
|
||||
if(c >= 'a' && c <= 'z') {
|
||||
return String.fromCodePoint(mathBoldLowerA + (c.charCodeAt(0) - 'a'.charCodeAt(0)));
|
||||
} else if(c >= 'A' && c <= 'Z') {
|
||||
return String.fromCodePoint(mathBoldUpperA + (c.charCodeAt(0) - 'A'.charCodeAt(0)));
|
||||
} else {
|
||||
return c;
|
||||
}
|
||||
});
|
||||
|
||||
return asSMP.join('');
|
||||
}
|
||||
|
||||
describe('ContextToken', function() {
|
||||
before(() => {
|
||||
KMWString.enableSupplementaryPlane(true);
|
||||
});
|
||||
|
||||
describe("<constructor>", () => {
|
||||
it("(model: LexicalModel)", async () => {
|
||||
let token = new ContextToken(plainModel);
|
||||
|
|
@ -73,4 +101,308 @@ describe('ContextToken', function() {
|
|||
assert.deepEqual({...clonedToken, searchSpace: null}, {...baseToken, searchSpace: null});
|
||||
});
|
||||
});
|
||||
|
||||
describe("splitToken()", () => {
|
||||
it("handles clean two-way split correctly", () => {
|
||||
// Setup phase
|
||||
const keystrokeDistributions: Distribution<Transform>[] = [
|
||||
[
|
||||
{ sample: { insert: 'c', deleteLeft: 0 }, p: 0.75 },
|
||||
{ sample: { insert: 't', deleteLeft: 0 }, p: 0.25 }
|
||||
],
|
||||
[
|
||||
{ sample: { insert: 'a', deleteLeft: 0 }, p: 0.75 },
|
||||
{ sample: { insert: 'o', deleteLeft: 0 }, p: 0.25 }
|
||||
],
|
||||
[
|
||||
{ sample: { insert: 'n', deleteLeft: 0 }, p: 0.75 },
|
||||
{ sample: { insert: 'r', deleteLeft: 0 }, p: 0.25 }
|
||||
],
|
||||
[
|
||||
{ sample: { insert: '\'', deleteLeft: 0 }, p: 0.75 },
|
||||
{ sample: { insert: 't', deleteLeft: 0 }, p: 0.25 }
|
||||
]
|
||||
]
|
||||
|
||||
const tokenToSplit = new ContextToken(plainModel);
|
||||
for(let i = 0; i < keystrokeDistributions.length; i++) {
|
||||
tokenToSplit.addInput({trueTransform: keystrokeDistributions[i][0].sample, inputStartIndex: 0}, keystrokeDistributions[i]);
|
||||
};
|
||||
|
||||
assert.equal(tokenToSplit.sourceText, 'can\'');
|
||||
assert.deepEqual(tokenToSplit.searchSpace.inputSequence, keystrokeDistributions);
|
||||
|
||||
// And now for the "fun" part.
|
||||
const resultsOfSplit = tokenToSplit.split({
|
||||
// Input portion here can be ignored.
|
||||
input: {
|
||||
text: 'can\'',
|
||||
index: 0
|
||||
}, matches: [
|
||||
// For this part, the text entries are what really matters.
|
||||
{ text: 'can', index: 0, textOffset: 0 },
|
||||
{ text: '\'', index: 1, textOffset: 3 }
|
||||
]
|
||||
}, plainModel);
|
||||
|
||||
assert.equal(resultsOfSplit.length, 2);
|
||||
assert.sameOrderedMembers(resultsOfSplit.map(t => t.exampleInput), ['can', '\'']);
|
||||
assert.sameDeepOrderedMembers(resultsOfSplit.map(t => t.searchSpace.inputSequence), [
|
||||
keystrokeDistributions.slice(0, 3),
|
||||
[keystrokeDistributions[3]]
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles mid-transform splits correctly", () => {
|
||||
// Setup phase
|
||||
const keystrokeDistributions: Distribution<Transform>[] = [
|
||||
[
|
||||
{ sample: { insert: 'biglargetransform', deleteLeft: 0, deleteRight: 0 }, p: 1 },
|
||||
]
|
||||
];
|
||||
const splitTextArray = ['big', 'large', 'transform'];
|
||||
|
||||
const tokenToSplit = new ContextToken(plainModel);
|
||||
for(let i = 0; i < keystrokeDistributions.length; i++) {
|
||||
tokenToSplit.addInput({trueTransform: keystrokeDistributions[i][0].sample, inputStartIndex: 0}, keystrokeDistributions[i]);
|
||||
};
|
||||
|
||||
assert.equal(tokenToSplit.sourceText, 'biglargetransform');
|
||||
assert.deepEqual(tokenToSplit.searchSpace.inputSequence, keystrokeDistributions);
|
||||
|
||||
// And now for the "fun" part.
|
||||
const resultsOfSplit = tokenToSplit.split({
|
||||
// Input portion here can be ignored.
|
||||
input: {
|
||||
text: 'biglargetransform',
|
||||
index: 0
|
||||
}, matches: [
|
||||
// For this part, the text entries are what really matters.
|
||||
{ text: 'big', index: 0, textOffset: 0 },
|
||||
{ text: 'large', index: 1, textOffset: 3 },
|
||||
{ text: 'transform', index: 2, textOffset: 8 }
|
||||
]
|
||||
}, plainModel);
|
||||
|
||||
assert.equal(resultsOfSplit.length, 3);
|
||||
assert.sameOrderedMembers(resultsOfSplit.map(t => t.exampleInput), splitTextArray);
|
||||
assert.sameDeepOrderedMembers(resultsOfSplit.map(t => t.inputRange[0]), [0, 3, 8].map(i => ({
|
||||
trueTransform: {
|
||||
insert: 'biglargetransform',
|
||||
deleteLeft: 0,
|
||||
deleteRight: 0
|
||||
}, inputStartIndex: i
|
||||
})));
|
||||
assert.sameDeepOrderedMembers(resultsOfSplit.map(t => t.searchSpace.inputSequence[0]), splitTextArray.map(t => [{
|
||||
sample: { insert: t, deleteLeft: 0, deleteRight: 0 }, p: 1
|
||||
}]));
|
||||
});
|
||||
|
||||
it("handles messy mid-transform splits correctly", () => {
|
||||
// Setup phase
|
||||
const keystrokeDistributions: Distribution<Transform>[] = [
|
||||
[
|
||||
{ sample: { insert: 'long', deleteLeft: 0, deleteRight: 0, id: 11 }, p: 1 }
|
||||
], [
|
||||
{ sample: { insert: 'argelovely', deleteLeft: 3, deleteRight: 0, id: 12 }, p: 1 }
|
||||
], [
|
||||
{ sample: { insert: 'ngtransforms', deleteLeft: 4, deleteRight: 0, id: 13 }, p: 1 }
|
||||
]
|
||||
];
|
||||
const splitTextArray = ['large', 'long', 'transforms'];
|
||||
|
||||
const tokenToSplit = new ContextToken(plainModel);
|
||||
for(let i = 0; i < keystrokeDistributions.length; i++) {
|
||||
tokenToSplit.addInput({trueTransform: keystrokeDistributions[i][0].sample, inputStartIndex: 0}, keystrokeDistributions[i]);
|
||||
};
|
||||
|
||||
assert.equal(tokenToSplit.exampleInput, 'largelongtransforms');
|
||||
assert.deepEqual(tokenToSplit.searchSpace.inputSequence, keystrokeDistributions);
|
||||
|
||||
// And now for the "fun" part.
|
||||
const resultsOfSplit = tokenToSplit.split({
|
||||
// Input portion here can be ignored.
|
||||
input: {
|
||||
text: 'largelongtransforms',
|
||||
index: 0
|
||||
}, matches: [
|
||||
// For this part, the text entries are what really matters.
|
||||
{ text: 'large', index: 0, textOffset: 0 },
|
||||
{ text: 'long', index: 1, textOffset: 5 },
|
||||
{ text: 'transforms', index: 2, textOffset: 9 }
|
||||
]
|
||||
}, plainModel);
|
||||
|
||||
assert.equal(resultsOfSplit.length, 3);
|
||||
assert.sameOrderedMembers(resultsOfSplit.map(t => t.exampleInput), splitTextArray);
|
||||
assert.deepEqual(resultsOfSplit[0].inputRange, [
|
||||
{ trueTransform: keystrokeDistributions[0][0].sample, inputStartIndex: 0 },
|
||||
{ trueTransform: keystrokeDistributions[1][0].sample, inputStartIndex: 0 },
|
||||
]);
|
||||
assert.deepEqual(resultsOfSplit[1].inputRange, [
|
||||
{ trueTransform: keystrokeDistributions[1][0].sample, inputStartIndex: 'arge'.length },
|
||||
{ trueTransform: keystrokeDistributions[2][0].sample, inputStartIndex: 0 },
|
||||
]);
|
||||
assert.deepEqual(resultsOfSplit[2].inputRange, [
|
||||
{ trueTransform: keystrokeDistributions[2][0].sample, inputStartIndex: 'ng'.length }
|
||||
]);
|
||||
|
||||
assert.deepEqual(resultsOfSplit[0].searchSpace.inputSequence, [
|
||||
keystrokeDistributions[0],
|
||||
keystrokeDistributions[1].map((entry) => {
|
||||
return {
|
||||
sample: {
|
||||
...entry.sample,
|
||||
insert: entry.sample.insert.slice(0, 4) // gets the 'arge' portion & the deleteLefts.
|
||||
}, p: entry.p
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
assert.deepEqual(resultsOfSplit[1].searchSpace.inputSequence, [
|
||||
keystrokeDistributions[1].map((entry) => {
|
||||
return {
|
||||
sample: {
|
||||
...entry.sample,
|
||||
insert: entry.sample.insert.slice('arge'.length),
|
||||
deleteLeft: 0
|
||||
}, p: entry.p
|
||||
}
|
||||
}),
|
||||
keystrokeDistributions[2].map((entry) => {
|
||||
return {
|
||||
sample: {
|
||||
...entry.sample,
|
||||
insert: entry.sample.insert.slice(0, 'ng'.length), // gets the 'ng' portion.
|
||||
}, p: entry.p
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
assert.deepEqual(resultsOfSplit[2].searchSpace.inputSequence, [
|
||||
keystrokeDistributions[2].map((entry) => {
|
||||
return {
|
||||
sample: {
|
||||
...entry.sample,
|
||||
insert: entry.sample.insert.slice('ng'.length), // drops the 'ng' portion.
|
||||
deleteLeft: 0
|
||||
}, p: entry.p
|
||||
}
|
||||
}),
|
||||
]);
|
||||
});
|
||||
|
||||
it("handles messy mid-transform splits correctly - non-BMP text", () => {
|
||||
// Setup phase
|
||||
const keystrokeDistributions: Distribution<Transform>[] = [
|
||||
[
|
||||
{ sample: { insert: toMathematicalSMP('long'), deleteLeft: 0, deleteRight: 0, id: 11 }, p: 1 }
|
||||
], [
|
||||
{ sample: { insert: toMathematicalSMP('argelovely'), deleteLeft: 3, deleteRight: 0, id: 12 }, p: 1 }
|
||||
], [
|
||||
{ sample: { insert: toMathematicalSMP('ngtransforms'), deleteLeft: 4, deleteRight: 0, id: 13 }, p: 1 }
|
||||
]
|
||||
];
|
||||
const splitTextArray = ['large', 'long', 'transforms'].map(t => toMathematicalSMP(t));
|
||||
|
||||
const tokenToSplit = new ContextToken(plainModel);
|
||||
for(let i = 0; i < keystrokeDistributions.length; i++) {
|
||||
tokenToSplit.addInput({trueTransform: keystrokeDistributions[i][0].sample, inputStartIndex: 0}, keystrokeDistributions[i]);
|
||||
};
|
||||
|
||||
assert.equal(tokenToSplit.exampleInput, toMathematicalSMP('largelongtransforms'));
|
||||
assert.deepEqual(tokenToSplit.searchSpace.inputSequence, keystrokeDistributions);
|
||||
|
||||
// And now for the "fun" part.
|
||||
const resultsOfSplit = tokenToSplit.split({
|
||||
// Input portion here can be ignored.
|
||||
input: {
|
||||
text: toMathematicalSMP('largelongtransforms'),
|
||||
index: 0
|
||||
}, matches: [
|
||||
// For this part, the text entries are what really matters.
|
||||
{ text: toMathematicalSMP('large'), index: 0, textOffset: 0 },
|
||||
{ text: toMathematicalSMP('long'), index: 1, textOffset: 5 },
|
||||
{ text: toMathematicalSMP('transforms'), index: 2, textOffset: 9 }
|
||||
]
|
||||
}, plainModel);
|
||||
|
||||
assert.equal(resultsOfSplit.length, 3);
|
||||
assert.sameOrderedMembers(resultsOfSplit.map(t => t.exampleInput), splitTextArray);
|
||||
assert.deepEqual(resultsOfSplit[0].inputRange, [
|
||||
{ trueTransform: keystrokeDistributions[0][0].sample, inputStartIndex: 0 },
|
||||
{ trueTransform: keystrokeDistributions[1][0].sample, inputStartIndex: 0 },
|
||||
]);
|
||||
assert.deepEqual(resultsOfSplit[1].inputRange, [
|
||||
{ trueTransform: keystrokeDistributions[1][0].sample, inputStartIndex: 'arge'.length },
|
||||
{ trueTransform: keystrokeDistributions[2][0].sample, inputStartIndex: 0 },
|
||||
]);
|
||||
assert.deepEqual(resultsOfSplit[2].inputRange, [
|
||||
{ trueTransform: keystrokeDistributions[2][0].sample, inputStartIndex: 'ng'.length }
|
||||
]);
|
||||
|
||||
assert.deepEqual(resultsOfSplit[0].searchSpace.inputSequence, [
|
||||
keystrokeDistributions[0],
|
||||
keystrokeDistributions[1].map((entry) => {
|
||||
return {
|
||||
sample: {
|
||||
...entry.sample,
|
||||
insert: KMWString.substring(entry.sample.insert, 0, 4) // gets the 'arge' portion & the deleteLefts.
|
||||
}, p: entry.p
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
assert.deepEqual(resultsOfSplit[1].searchSpace.inputSequence, [
|
||||
keystrokeDistributions[1].map((entry) => {
|
||||
return {
|
||||
sample: {
|
||||
...entry.sample,
|
||||
insert: KMWString.substring(entry.sample.insert, 'arge'.length),
|
||||
deleteLeft: 0
|
||||
}, p: entry.p
|
||||
}
|
||||
}),
|
||||
keystrokeDistributions[2].map((entry) => {
|
||||
return {
|
||||
sample: {
|
||||
...entry.sample,
|
||||
insert: KMWString.substring(entry.sample.insert, 0, 'ng'.length), // gets the 'ng' portion.
|
||||
}, p: entry.p
|
||||
}
|
||||
}),
|
||||
]);
|
||||
|
||||
assert.deepEqual(resultsOfSplit[2].searchSpace.inputSequence, [
|
||||
keystrokeDistributions[2].map((entry) => {
|
||||
return {
|
||||
sample: {
|
||||
...entry.sample,
|
||||
insert: KMWString.substring(entry.sample.insert, 'ng'.length), // drops the 'ng' portion.
|
||||
deleteLeft: 0
|
||||
}, p: entry.p
|
||||
}
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('preprocessInputSources', () => {
|
||||
it('properly preprocesses deleteLefts in the transforms', () => {
|
||||
const transforms: Transform[] = [
|
||||
{ insert: 'long', deleteLeft: 0, deleteRight: 0 },
|
||||
{ insert: 'argelovely', deleteLeft: 3, deleteRight: 0 },
|
||||
{ insert: 'ngtransforms', deleteLeft: 4, deleteRight: 0 }
|
||||
];
|
||||
|
||||
const results = preprocessInputSources(transforms.map((t) => ({
|
||||
trueTransform: t,
|
||||
inputStartIndex: 0
|
||||
})));
|
||||
|
||||
assert.equal(results.length, transforms.length);
|
||||
assert.sameOrderedMembers(results.map((entry) => entry.trueTransform.insert), ['l', 'argelo', 'ngtransforms']);
|
||||
assert.sameOrderedMembers(results.map((entry) => entry.trueTransform.deleteLeft), [0, 0, 0]);
|
||||
});
|
||||
});
|
||||
Loading…
Add table
Reference in a new issue