Skip to content

Commit

Permalink
[DataEntry] Give longer gloss spelling suggestions (#2842)
Browse files Browse the repository at this point in the history
  • Loading branch information
imnasnainaec authored Jan 9, 2024
1 parent 3dfd736 commit 32c22c7
Show file tree
Hide file tree
Showing 4 changed files with 74 additions and 19 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,6 @@ export default function GlossWithSuggestions(
): ReactElement {
const spellChecker = useContext(SpellCheckerContext);

const maxSuggestions = 5;

useEffect(() => {
if (props.onUpdate) {
props.onUpdate();
Expand All @@ -39,11 +37,8 @@ export default function GlossWithSuggestions(
<Autocomplete
id={props.textFieldId}
disabled={props.isDisabled}
filterOptions={(options: string[]) =>
options.length <= maxSuggestions
? options
: options.slice(0, maxSuggestions)
}
// there's a bug with disappearing options if filterOptions isn't specified
filterOptions={(options) => options}
// freeSolo allows use of a typed entry not available as a drop-down option
freeSolo
includeInputInList
Expand Down
36 changes: 33 additions & 3 deletions src/utilities/spellChecker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,12 @@ interface SplitWord {
final?: string;
}

const maxSuggestions = 5;

export default class SpellChecker {
private bcp47: Bcp47Code | undefined;
private dictLoader: DictionaryLoader | undefined;
private dictLoaded: { [key: string]: string[] } = {};
private spell: nspell | undefined;

constructor(lang?: string) {
Expand All @@ -29,9 +32,11 @@ export default class SpellChecker {

this.bcp47 = bcp47;
this.dictLoader = new DictionaryLoader(bcp47);
this.dictLoaded = {};
await this.dictLoader.loadDictionary().then((dic) => {
if (dic !== undefined) {
this.spell = nspell("SET UTF-8", dic);
this.addToDictLoaded(dic);
if (process.env.NODE_ENV === "development") {
console.log(`Loaded spell-checker: ${bcp47}`);
}
Expand All @@ -47,6 +52,7 @@ export default class SpellChecker {

const part = await this.dictLoader.loadDictPart(word);
if (part) {
this.addToDictLoaded(part);
this.spell.personal(part);
}
}
Expand All @@ -55,6 +61,15 @@ export default class SpellChecker {
return this.spell?.correct(word);
}

addToDictLoaded(entries: string): void {
entries.split("\n").map((w) => {
if (!(w[0] in this.dictLoaded)) {
this.dictLoaded[w[0]] = [];
}
this.dictLoaded[w[0]].push(w);
});
}

static cleanAndSplit(word: string): SplitWord {
// Trim whitespace from the start and non-letter/-mark/-number characters from the end.
// Use of \p{L}\p{M}\p{N} here matches that in split_dictionary.py.
Expand Down Expand Up @@ -88,15 +103,30 @@ export default class SpellChecker {
// Don't await--just load for future use.
this.load(final);

// Get spelling suggestions.
let suggestions = this.spell.suggest(final);
if (!suggestions.length) {
// Extend the current word to get suggestions 1 or 2 characters longer.
suggestions = this.spell.suggest(`${final}..`);

// Add lookahead suggestions.
if (this.dictLoaded[final[0]] && suggestions.length < maxSuggestions) {
const lookahead = this.dictLoaded[final[0]].filter(
(entry) =>
entry.length >= final.length &&
entry.substring(0, final.length) === final &&
!suggestions.includes(entry)
);
suggestions.push(...lookahead.sort());
}

// Limit to maxSuggestions.
if (suggestions.length > maxSuggestions) {
suggestions = suggestions.slice(0, maxSuggestions);
}

// Prepend the start of the typed phrase, if any.
if (suggestions.length && allButFinal) {
suggestions = suggestions.map((w) => allButFinal + w);
}

return suggestions;
}
}
2 changes: 1 addition & 1 deletion src/utilities/tests/dictionaryLoader.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ const bcp47 = Bcp47Code.Es;

describe("DictionaryLoader", () => {
describe("constructor", () => {
it(" ets lang and gets keys", () => {
it("gets lang and gets keys", () => {
const loader = new DictionaryLoader(bcp47);
expect(loader.lang === bcp47);
expect(mockGetKeys).toHaveBeenCalledTimes(1);
Expand Down
46 changes: 38 additions & 8 deletions src/utilities/tests/spellChecker.test.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,24 @@
import SpellChecker from "utilities/spellChecker";

jest.mock("resources/dictionaries", () => ({
getDict: () => Promise.resolve(`1\n${mockValidWord}`),
getDict: () => Promise.resolve(`2\n${mockValidWordA}\n${mockValidWordBExt}`),
getKeys: () => [],
}));

const mockValidWord = "mockWord";
const invalidWord = "asdfghjkl";
const mockWord = "mockWord";
const mockValidWordA = `${mockWord}A`;
const mockWordB = `${mockWord}B`;
const mockWordC = `${mockWord}C`;
const mockValidWordBExt = `${mockWordB}Extended`;

describe("SpellChecker", () => {
describe("correct", () => {
it("detects a correctly spelled word", (done) => {
const spellChecker = new SpellChecker("en");
// Give the dictionary half-a-sec to load.
setTimeout(() => {
expect(spellChecker.correct(mockValidWord)).toEqual(true);
expect(spellChecker.correct(mockValidWordA)).toEqual(true);
done();
}, 500);
});
Expand All @@ -22,7 +27,7 @@ describe("SpellChecker", () => {
const spellChecker = new SpellChecker("en");
// Give the dictionary half-a-sec to load.
setTimeout(() => {
expect(spellChecker.correct("abjkdsjf")).toEqual(false);
expect(spellChecker.correct(invalidWord)).toEqual(false);
done();
}, 500);
});
Expand Down Expand Up @@ -73,13 +78,38 @@ describe("SpellChecker", () => {
});

describe("getSpellingSuggestions", () => {
it("returns suggestions", (done) => {
it("returns nothing for gibberish", (done) => {
const spellChecker = new SpellChecker("en");
// Give the dictionary half-a-sec to load.
setTimeout(() => {
expect(
spellChecker.getSpellingSuggestions(`${mockValidWord}`)
).toHaveLength(1);
const suggestions = spellChecker.getSpellingSuggestions(invalidWord);
expect(suggestions).toHaveLength(0);
done();
}, 500);
});

it("returns spelling correction", (done) => {
const spellChecker = new SpellChecker("en");
// Give the dictionary half-a-sec to load.
setTimeout(() => {
const suggestions = spellChecker.getSpellingSuggestions(mockWordC);
// Returns suggestion with 1 letter different.
expect(suggestions).toContain(mockValidWordA);
// Don't return lookahead for word 1 letter different.
expect(suggestions).not.toContain(mockValidWordBExt);
done();
}, 500);
});

it("returns spelling correction and lookahead", (done) => {
const spellChecker = new SpellChecker("en");
// Give the dictionary half-a-sec to load.
setTimeout(() => {
const suggestions = spellChecker.getSpellingSuggestions(mockWordB);
// Returns suggestion with 1 letter different.
expect(suggestions).toContain(mockValidWordA);
// Returns suggestions with many letters added.
expect(suggestions).toContain(mockValidWordBExt);
done();
}, 500);
});
Expand Down

0 comments on commit 32c22c7

Please sign in to comment.