| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247 |
- import Hunspell from './wasm/hunspell'
- import hunspellWasmPath from './wasm/hunspell.wasm'
- import { buildAdditionalDictionary } from './wordlists/dictionary-additions'
- type SpellChecker = {
- spell(words: string[]): { index: number }[]
- suggest(word: string): string[]
- addWord(word: string): void
- removeWord(word: string): void
- destroy(): void
- }
- const createSpellChecker = async ({
- lang,
- learnedWords,
- baseAssetPath,
- dictionariesRoot,
- }: {
- lang: string
- learnedWords: string[]
- baseAssetPath: string
- dictionariesRoot: string
- }) => {
- const fileLocations: Record<string, string> = {
- 'hunspell.wasm': new URL(hunspellWasmPath, baseAssetPath).toString(),
- }
- const hunspell = await Hunspell({
- locateFile(file: string) {
- return fileLocations[file]
- },
- })
- const {
- cwrap,
- FS,
- MEMFS,
- stringToNewUTF8,
- _malloc,
- _free,
- getValue,
- UTF8ToString,
- } = hunspell
- // https://github.com/hunspell/hunspell/blob/master/src/hunspell/hunspell.h
- // https://github.com/kwonoj/hunspell-asm/blob/master/src/wrapHunspellInterface.ts
- const create = cwrap('Hunspell_create', 'number', ['number', 'number'])
- const destroy = cwrap('Hunspell_destroy', 'number', ['number', 'number'])
- const spell = cwrap('Hunspell_spell', 'number', ['number', 'number'])
- const suggest = cwrap('Hunspell_suggest', 'number', [
- 'number',
- 'number',
- 'number',
- ])
- const addDic = cwrap('Hunspell_add_dic', 'number', ['number', 'number'])
- const addWord = cwrap('Hunspell_add', 'number', ['number', 'number'])
- const removeWord = cwrap('Hunspell_remove', 'number', ['number', 'number'])
- const freeList = cwrap('Hunspell_free_list', 'number', [
- 'number',
- 'number',
- 'number',
- ])
- FS.mkdir('/dictionaries')
- const dictionariesRootURL = new URL(dictionariesRoot, baseAssetPath)
- const [dic, aff] = await Promise.all([
- fetch(new URL(`./${lang}.dic`, dictionariesRootURL)).then(response =>
- response.arrayBuffer()
- ),
- fetch(new URL(`./${lang}.aff`, dictionariesRootURL)).then(response =>
- response.arrayBuffer()
- ),
- ])
- FS.mount(MEMFS, {}, '/dictionaries')
- FS.writeFile('/dictionaries/index.dic', new Uint8Array(dic))
- FS.writeFile('/dictionaries/index.aff', new Uint8Array(aff))
- const dicPtr = stringToNewUTF8('/dictionaries/index.dic')
- const affPtr = stringToNewUTF8('/dictionaries/index.aff')
- const spellPtr = create(affPtr, dicPtr)
- FS.writeFile(
- '/dictionaries/extra.dic',
- await buildAdditionalDictionary(lang, learnedWords)
- )
- const extraDicPtr = stringToNewUTF8('/dictionaries/extra.dic')
- addDic(spellPtr, extraDicPtr)
- _free(extraDicPtr)
- const spellChecker: SpellChecker = {
- spell(words) {
- const misspellings: { index: number }[] = []
- for (const [index, word] of words.entries()) {
- const wordPtr = stringToNewUTF8(word)
- const spellResult = spell(spellPtr, wordPtr)
- _free(wordPtr)
- if (spellResult === 0) {
- misspellings.push({ index })
- }
- }
- return misspellings
- },
- suggest(word) {
- const suggestions: string[] = []
- const suggestionListPtr = _malloc(4)
- const wordPtr = stringToNewUTF8(word)
- const suggestionCount = suggest(spellPtr, suggestionListPtr, wordPtr)
- _free(wordPtr)
- const suggestionListValuePtr = getValue(suggestionListPtr, '*')
- for (let i = 0; i < suggestionCount; i++) {
- const suggestion = UTF8ToString(
- getValue(suggestionListValuePtr + i * 4, '*')
- )
- suggestions.push(suggestion)
- }
- freeList(spellPtr, suggestionListPtr, suggestionCount)
- _free(suggestionListPtr)
- return suggestions
- },
- addWord(word) {
- const wordPtr = stringToNewUTF8(word)
- const result = addWord(spellPtr, wordPtr)
- _free(wordPtr)
- if (result !== 0) {
- throw new Error('The word could not be added to the dictionary')
- }
- },
- removeWord(word) {
- const wordPtr = stringToNewUTF8(word)
- const result = removeWord(spellPtr, wordPtr)
- _free(wordPtr)
- if (result !== 0) {
- throw new Error('The word could not be removed from the dictionary')
- }
- },
- destroy() {
- destroy(spellPtr)
- _free(spellPtr)
- _free(dicPtr)
- _free(affPtr)
- },
- }
- return spellChecker
- }
- let spellCheckerPromise: Promise<SpellChecker>
- self.addEventListener('message', async event => {
- switch (event.data.type) {
- case 'init':
- try {
- spellCheckerPromise = createSpellChecker(event.data)
- await spellCheckerPromise
- self.postMessage({ loaded: true })
- } catch (error) {
- console.error(error)
- self.postMessage({ loadingFailed: error })
- }
- break
- case 'spell':
- {
- const { id, words } = event.data
- try {
- const spellChecker = await spellCheckerPromise
- const misspellings = spellChecker.spell(words)
- self.postMessage({ id, misspellings })
- } catch (error) {
- console.error(error)
- self.postMessage({ id, error: true })
- }
- }
- break
- case 'suggest':
- {
- const { id, word } = event.data
- try {
- const spellChecker = await spellCheckerPromise
- const suggestions = spellChecker.suggest(word)
- self.postMessage({ id, suggestions })
- } catch (error) {
- console.error(error)
- self.postMessage({ id, error: true })
- }
- }
- break
- case 'add_word':
- {
- const { id, word } = event.data
- try {
- const spellChecker = await spellCheckerPromise
- spellChecker.addWord(word)
- self.postMessage({ id })
- } catch (error) {
- console.error(error)
- self.postMessage({ id, error: true })
- }
- }
- break
- case 'remove_word':
- {
- const { id, word } = event.data
- try {
- const spellChecker = await spellCheckerPromise
- spellChecker.removeWord(word)
- self.postMessage({ id })
- } catch (error) {
- console.error(error)
- self.postMessage({ id, error: true })
- }
- }
- break
- case 'destroy':
- {
- const { id } = event.data
- try {
- const spellChecker = await spellCheckerPromise
- spellChecker.destroy()
- self.postMessage({ id })
- } catch (error) {
- console.error(error)
- self.postMessage({ id, error: true })
- }
- }
- break
- }
- })
- self.postMessage({ listening: true })
|