hunspell.worker.ts 6.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247
  1. import Hunspell from './wasm/hunspell'
  2. import hunspellWasmPath from './wasm/hunspell.wasm'
  3. import { buildAdditionalDictionary } from './wordlists/dictionary-additions'
  4. type SpellChecker = {
  5. spell(words: string[]): { index: number }[]
  6. suggest(word: string): string[]
  7. addWord(word: string): void
  8. removeWord(word: string): void
  9. destroy(): void
  10. }
  11. const createSpellChecker = async ({
  12. lang,
  13. learnedWords,
  14. baseAssetPath,
  15. dictionariesRoot,
  16. }: {
  17. lang: string
  18. learnedWords: string[]
  19. baseAssetPath: string
  20. dictionariesRoot: string
  21. }) => {
  22. const fileLocations: Record<string, string> = {
  23. 'hunspell.wasm': new URL(hunspellWasmPath, baseAssetPath).toString(),
  24. }
  25. const hunspell = await Hunspell({
  26. locateFile(file: string) {
  27. return fileLocations[file]
  28. },
  29. })
  30. const {
  31. cwrap,
  32. FS,
  33. MEMFS,
  34. stringToNewUTF8,
  35. _malloc,
  36. _free,
  37. getValue,
  38. UTF8ToString,
  39. } = hunspell
  40. // https://github.com/hunspell/hunspell/blob/master/src/hunspell/hunspell.h
  41. // https://github.com/kwonoj/hunspell-asm/blob/master/src/wrapHunspellInterface.ts
  42. const create = cwrap('Hunspell_create', 'number', ['number', 'number'])
  43. const destroy = cwrap('Hunspell_destroy', 'number', ['number', 'number'])
  44. const spell = cwrap('Hunspell_spell', 'number', ['number', 'number'])
  45. const suggest = cwrap('Hunspell_suggest', 'number', [
  46. 'number',
  47. 'number',
  48. 'number',
  49. ])
  50. const addDic = cwrap('Hunspell_add_dic', 'number', ['number', 'number'])
  51. const addWord = cwrap('Hunspell_add', 'number', ['number', 'number'])
  52. const removeWord = cwrap('Hunspell_remove', 'number', ['number', 'number'])
  53. const freeList = cwrap('Hunspell_free_list', 'number', [
  54. 'number',
  55. 'number',
  56. 'number',
  57. ])
  58. FS.mkdir('/dictionaries')
  59. const dictionariesRootURL = new URL(dictionariesRoot, baseAssetPath)
  60. const [dic, aff] = await Promise.all([
  61. fetch(new URL(`./${lang}.dic`, dictionariesRootURL)).then(response =>
  62. response.arrayBuffer()
  63. ),
  64. fetch(new URL(`./${lang}.aff`, dictionariesRootURL)).then(response =>
  65. response.arrayBuffer()
  66. ),
  67. ])
  68. FS.mount(MEMFS, {}, '/dictionaries')
  69. FS.writeFile('/dictionaries/index.dic', new Uint8Array(dic))
  70. FS.writeFile('/dictionaries/index.aff', new Uint8Array(aff))
  71. const dicPtr = stringToNewUTF8('/dictionaries/index.dic')
  72. const affPtr = stringToNewUTF8('/dictionaries/index.aff')
  73. const spellPtr = create(affPtr, dicPtr)
  74. FS.writeFile(
  75. '/dictionaries/extra.dic',
  76. await buildAdditionalDictionary(lang, learnedWords)
  77. )
  78. const extraDicPtr = stringToNewUTF8('/dictionaries/extra.dic')
  79. addDic(spellPtr, extraDicPtr)
  80. _free(extraDicPtr)
  81. const spellChecker: SpellChecker = {
  82. spell(words) {
  83. const misspellings: { index: number }[] = []
  84. for (const [index, word] of words.entries()) {
  85. const wordPtr = stringToNewUTF8(word)
  86. const spellResult = spell(spellPtr, wordPtr)
  87. _free(wordPtr)
  88. if (spellResult === 0) {
  89. misspellings.push({ index })
  90. }
  91. }
  92. return misspellings
  93. },
  94. suggest(word) {
  95. const suggestions: string[] = []
  96. const suggestionListPtr = _malloc(4)
  97. const wordPtr = stringToNewUTF8(word)
  98. const suggestionCount = suggest(spellPtr, suggestionListPtr, wordPtr)
  99. _free(wordPtr)
  100. const suggestionListValuePtr = getValue(suggestionListPtr, '*')
  101. for (let i = 0; i < suggestionCount; i++) {
  102. const suggestion = UTF8ToString(
  103. getValue(suggestionListValuePtr + i * 4, '*')
  104. )
  105. suggestions.push(suggestion)
  106. }
  107. freeList(spellPtr, suggestionListPtr, suggestionCount)
  108. _free(suggestionListPtr)
  109. return suggestions
  110. },
  111. addWord(word) {
  112. const wordPtr = stringToNewUTF8(word)
  113. const result = addWord(spellPtr, wordPtr)
  114. _free(wordPtr)
  115. if (result !== 0) {
  116. throw new Error('The word could not be added to the dictionary')
  117. }
  118. },
  119. removeWord(word) {
  120. const wordPtr = stringToNewUTF8(word)
  121. const result = removeWord(spellPtr, wordPtr)
  122. _free(wordPtr)
  123. if (result !== 0) {
  124. throw new Error('The word could not be removed from the dictionary')
  125. }
  126. },
  127. destroy() {
  128. destroy(spellPtr)
  129. _free(spellPtr)
  130. _free(dicPtr)
  131. _free(affPtr)
  132. },
  133. }
  134. return spellChecker
  135. }
  136. let spellCheckerPromise: Promise<SpellChecker>
  137. self.addEventListener('message', async event => {
  138. switch (event.data.type) {
  139. case 'init':
  140. try {
  141. spellCheckerPromise = createSpellChecker(event.data)
  142. await spellCheckerPromise
  143. self.postMessage({ loaded: true })
  144. } catch (error) {
  145. console.error(error)
  146. self.postMessage({ loadingFailed: error })
  147. }
  148. break
  149. case 'spell':
  150. {
  151. const { id, words } = event.data
  152. try {
  153. const spellChecker = await spellCheckerPromise
  154. const misspellings = spellChecker.spell(words)
  155. self.postMessage({ id, misspellings })
  156. } catch (error) {
  157. console.error(error)
  158. self.postMessage({ id, error: true })
  159. }
  160. }
  161. break
  162. case 'suggest':
  163. {
  164. const { id, word } = event.data
  165. try {
  166. const spellChecker = await spellCheckerPromise
  167. const suggestions = spellChecker.suggest(word)
  168. self.postMessage({ id, suggestions })
  169. } catch (error) {
  170. console.error(error)
  171. self.postMessage({ id, error: true })
  172. }
  173. }
  174. break
  175. case 'add_word':
  176. {
  177. const { id, word } = event.data
  178. try {
  179. const spellChecker = await spellCheckerPromise
  180. spellChecker.addWord(word)
  181. self.postMessage({ id })
  182. } catch (error) {
  183. console.error(error)
  184. self.postMessage({ id, error: true })
  185. }
  186. }
  187. break
  188. case 'remove_word':
  189. {
  190. const { id, word } = event.data
  191. try {
  192. const spellChecker = await spellCheckerPromise
  193. spellChecker.removeWord(word)
  194. self.postMessage({ id })
  195. } catch (error) {
  196. console.error(error)
  197. self.postMessage({ id, error: true })
  198. }
  199. }
  200. break
  201. case 'destroy':
  202. {
  203. const { id } = event.data
  204. try {
  205. const spellChecker = await spellCheckerPromise
  206. spellChecker.destroy()
  207. self.postMessage({ id })
  208. } catch (error) {
  209. console.error(error)
  210. self.postMessage({ id, error: true })
  211. }
  212. }
  213. break
  214. }
  215. })
  216. self.postMessage({ listening: true })