hunspell.worker.ts 5.9 KB

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