hunspell.worker.ts 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250
  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. WORKERFS,
  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.blob()
  61. ),
  62. fetch(new URL(`./${lang}.aff`, dictionariesRootURL)).then(response =>
  63. response.blob()
  64. ),
  65. ])
  66. FS.mount(
  67. WORKERFS,
  68. {
  69. blobs: [
  70. { name: 'index.dic', data: dic },
  71. { name: 'index.aff', data: aff },
  72. ],
  73. },
  74. '/dictionaries'
  75. )
  76. const dicPtr = stringToNewUTF8('/dictionaries/index.dic')
  77. const affPtr = stringToNewUTF8('/dictionaries/index.aff')
  78. const spellPtr = create(affPtr, dicPtr)
  79. for (const word of learnedWords) {
  80. const wordPtr = stringToNewUTF8(word)
  81. addWord(spellPtr, wordPtr)
  82. _free(wordPtr)
  83. }
  84. const spellChecker: SpellChecker = {
  85. spell(words) {
  86. const misspellings: { index: number }[] = []
  87. for (const [index, word] of words.entries()) {
  88. const wordPtr = stringToNewUTF8(word)
  89. const spellResult = spell(spellPtr, wordPtr)
  90. _free(wordPtr)
  91. if (spellResult === 0) {
  92. misspellings.push({ index })
  93. }
  94. }
  95. return misspellings
  96. },
  97. suggest(word) {
  98. const suggestions: string[] = []
  99. const suggestionListPtr = _malloc(4)
  100. const wordPtr = stringToNewUTF8(word)
  101. const suggestionCount = suggest(spellPtr, suggestionListPtr, wordPtr)
  102. _free(wordPtr)
  103. const suggestionListValuePtr = getValue(suggestionListPtr, '*')
  104. for (let i = 0; i < suggestionCount; i++) {
  105. const suggestion = UTF8ToString(
  106. getValue(suggestionListValuePtr + i * 4, '*')
  107. )
  108. suggestions.push(suggestion)
  109. }
  110. freeList(spellPtr, suggestionListPtr, suggestionCount)
  111. _free(suggestionListPtr)
  112. return suggestions
  113. },
  114. addWord(word) {
  115. const wordPtr = stringToNewUTF8(word)
  116. const result = addWord(spellPtr, wordPtr)
  117. _free(wordPtr)
  118. if (result !== 0) {
  119. throw new Error('The word could not be added to the dictionary')
  120. }
  121. },
  122. removeWord(word) {
  123. const wordPtr = stringToNewUTF8(word)
  124. const result = removeWord(spellPtr, wordPtr)
  125. _free(wordPtr)
  126. if (result !== 0) {
  127. throw new Error('The word could not be removed from the dictionary')
  128. }
  129. },
  130. destroy() {
  131. destroy(spellPtr)
  132. _free(spellPtr)
  133. _free(dicPtr)
  134. _free(affPtr)
  135. },
  136. }
  137. return spellChecker
  138. }
  139. let spellCheckerPromise: Promise<SpellChecker>
  140. self.addEventListener('message', async event => {
  141. switch (event.data.type) {
  142. case 'init':
  143. try {
  144. spellCheckerPromise = createSpellChecker(event.data)
  145. await spellCheckerPromise
  146. self.postMessage({ loaded: true })
  147. } catch (error) {
  148. console.error(error)
  149. self.postMessage({ error: true })
  150. }
  151. break
  152. case 'spell':
  153. {
  154. const { id, words } = event.data
  155. try {
  156. const spellChecker = await spellCheckerPromise
  157. const misspellings = spellChecker.spell(words)
  158. self.postMessage({ id, misspellings })
  159. } catch (error) {
  160. console.error(error)
  161. self.postMessage({ id, error: true })
  162. }
  163. }
  164. break
  165. case 'suggest':
  166. {
  167. const { id, word } = event.data
  168. try {
  169. const spellChecker = await spellCheckerPromise
  170. const suggestions = spellChecker.suggest(word)
  171. self.postMessage({ id, suggestions })
  172. } catch (error) {
  173. console.error(error)
  174. self.postMessage({ id, error: true })
  175. }
  176. }
  177. break
  178. case 'add_word':
  179. {
  180. const { id, word } = event.data
  181. try {
  182. const spellChecker = await spellCheckerPromise
  183. spellChecker.addWord(word)
  184. self.postMessage({ id })
  185. } catch (error) {
  186. console.error(error)
  187. self.postMessage({ id, error: true })
  188. }
  189. }
  190. break
  191. case 'remove_word':
  192. {
  193. const { id, word } = event.data
  194. try {
  195. const spellChecker = await spellCheckerPromise
  196. spellChecker.removeWord(word)
  197. self.postMessage({ id })
  198. } catch (error) {
  199. console.error(error)
  200. self.postMessage({ id, error: true })
  201. }
  202. }
  203. break
  204. case 'destroy':
  205. {
  206. const { id } = event.data
  207. try {
  208. const spellChecker = await spellCheckerPromise
  209. spellChecker.destroy()
  210. self.postMessage({ id })
  211. } catch (error) {
  212. console.error(error)
  213. self.postMessage({ id, error: true })
  214. }
  215. }
  216. break
  217. }
  218. })
  219. self.postMessage({ listening: true })