ASpell.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Sanity-check the conversion and remove this comment.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS101: Remove unnecessary use of Array.from
  6. * DS102: Remove unnecessary code created because of implicit returns
  7. * DS207: Consider shorter variations of null checks
  8. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  9. */
  10. const ASpellWorkerPool = require('./ASpellWorkerPool')
  11. const LRU = require('lru-cache')
  12. const logger = require('logger-sharelatex')
  13. const fs = require('fs')
  14. const settings = require('settings-sharelatex')
  15. const Path = require('path')
  16. const { promisify } = require('util')
  17. const OneMinute = 60 * 1000
  18. const opts = { max: 10000, maxAge: OneMinute * 60 * 10 }
  19. const cache = new LRU(opts)
  20. const cacheFsPath = Path.resolve(settings.cacheDir, 'spell.cache')
  21. const cacheFsPathTmp = cacheFsPath + '.tmp'
  22. // load any existing cache
  23. try {
  24. const oldCache = fs.readFileSync(cacheFsPath)
  25. cache.load(JSON.parse(oldCache))
  26. } catch (error) {
  27. const err = error
  28. logger.log({ err, cacheFsPath }, 'could not load the cache file')
  29. }
  30. // write the cache every 30 minutes
  31. const cacheDump = setInterval(function() {
  32. const dump = JSON.stringify(cache.dump())
  33. return fs.writeFile(cacheFsPathTmp, dump, function(err) {
  34. if (err != null) {
  35. logger.log({ err }, 'error writing cache file')
  36. return fs.unlink(cacheFsPathTmp)
  37. } else {
  38. fs.rename(cacheFsPathTmp, cacheFsPath, err => {
  39. if (err) {
  40. logger.error({ err }, 'error renaming cache file')
  41. } else {
  42. logger.log({ len: dump.length, cacheFsPath }, 'wrote cache file')
  43. }
  44. })
  45. }
  46. })
  47. }, 30 * OneMinute)
  48. class ASpellRunner {
  49. checkWords(language, words, callback) {
  50. if (callback == null) {
  51. callback = () => {}
  52. }
  53. return this.runAspellOnWords(language, words, (error, output) => {
  54. if (error != null) {
  55. return callback(error)
  56. }
  57. // output = @removeAspellHeader(output)
  58. const suggestions = this.getSuggestions(language, output)
  59. const results = []
  60. let hits = 0
  61. const addToCache = {}
  62. for (let i = 0; i < words.length; i++) {
  63. const word = words[i]
  64. const key = language + ':' + word
  65. const cached = cache.get(key)
  66. if (cached != null) {
  67. hits++
  68. if (cached === true) {
  69. // valid word, no need to do anything
  70. continue
  71. } else {
  72. results.push({ index: i, suggestions: cached })
  73. }
  74. } else {
  75. if (suggestions[key] != null) {
  76. addToCache[key] = suggestions[key]
  77. results.push({ index: i, suggestions: suggestions[key] })
  78. } else {
  79. // a valid word, but uncached
  80. addToCache[key] = true
  81. }
  82. }
  83. }
  84. // update the cache after processing all words, to avoid cache
  85. // changing while we use it
  86. for (let k in addToCache) {
  87. const v = addToCache[k]
  88. cache.set(k, v)
  89. }
  90. logger.info(
  91. {
  92. hits,
  93. total: words.length,
  94. hitrate: (hits / words.length).toFixed(2)
  95. },
  96. 'cache hit rate'
  97. )
  98. return callback(null, results)
  99. })
  100. }
  101. getSuggestions(language, output) {
  102. const lines = output.split('\n')
  103. const suggestions = {}
  104. for (let line of Array.from(lines)) {
  105. var parts, word
  106. if (line[0] === '&') {
  107. // Suggestions found
  108. parts = line.split(' ')
  109. if (parts.length > 1) {
  110. word = parts[1]
  111. const suggestionsString = line.slice(line.indexOf(':') + 2)
  112. suggestions[language + ':' + word] = suggestionsString.split(', ')
  113. }
  114. } else if (line[0] === '#') {
  115. // No suggestions
  116. parts = line.split(' ')
  117. if (parts.length > 1) {
  118. word = parts[1]
  119. suggestions[language + ':' + word] = []
  120. }
  121. }
  122. }
  123. return suggestions
  124. }
  125. // removeAspellHeader: (output) -> output.slice(1)
  126. runAspellOnWords(language, words, callback) {
  127. // send words to aspell, get back string output for those words
  128. // find a free pipe for the language (or start one)
  129. // send the words down the pipe
  130. // send an END marker that will generate a "*" line in the output
  131. // when the output pipe receives the "*" return the data sofar and reset the pipe to be available
  132. //
  133. // @open(language)
  134. // @captureOutput(callback)
  135. // @setTerseMode()
  136. // start = new Date()
  137. if (callback == null) {
  138. callback = () => {}
  139. }
  140. const newWord = {}
  141. for (let word of Array.from(words)) {
  142. if (!newWord[word] && !cache.has(language + ':' + word)) {
  143. newWord[word] = true
  144. }
  145. }
  146. words = Object.keys(newWord)
  147. if (words.length) {
  148. return WorkerPool.check(language, words, ASpell.ASPELL_TIMEOUT, callback)
  149. } else {
  150. return callback(null, '')
  151. }
  152. }
  153. }
  154. const ASpell = {
  155. // The description of how to call aspell from another program can be found here:
  156. // http://aspell.net/man-html/Through-A-Pipe.html
  157. checkWords(language, words, callback) {
  158. if (callback == null) {
  159. callback = () => {}
  160. }
  161. const runner = new ASpellRunner()
  162. return runner.checkWords(language, words, callback)
  163. },
  164. ASPELL_TIMEOUT: 10000
  165. }
  166. const promises = {
  167. checkWords: promisify(ASpell.checkWords)
  168. }
  169. ASpell.promises = promises
  170. module.exports = ASpell
  171. var WorkerPool = new ASpellWorkerPool()
  172. module.exports.cacheDump = cacheDump