hunspell.worker.ts 6.2 KB

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