spellcheck.js 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478
  1. // Copyright 2007 The Closure Library Authors. All Rights Reserved.
  2. //
  3. // Licensed under the Apache License, Version 2.0 (the "License");
  4. // you may not use this file except in compliance with the License.
  5. // You may obtain a copy of the License at
  6. //
  7. // http://www.apache.org/licenses/LICENSE-2.0
  8. //
  9. // Unless required by applicable law or agreed to in writing, software
  10. // distributed under the License is distributed on an "AS-IS" BASIS,
  11. // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  12. // See the License for the specific language governing permissions and
  13. // limitations under the License.
  14. /**
  15. * @fileoverview Support class for spell checker components.
  16. *
  17. * @author eae@google.com (Emil A Eklund)
  18. */
  19. goog.provide('goog.spell.SpellCheck');
  20. goog.provide('goog.spell.SpellCheck.WordChangedEvent');
  21. goog.require('goog.Timer');
  22. goog.require('goog.events.Event');
  23. goog.require('goog.events.EventTarget');
  24. goog.require('goog.structs.Set');
  25. /**
  26. * Support class for spell checker components. Provides basic functionality
  27. * such as word lookup and caching.
  28. *
  29. * @param {Function=} opt_lookupFunction Function to use for word lookup. Must
  30. * accept an array of words, an object reference and a callback function as
  31. * parameters. It must also call the callback function (as a method on the
  32. * object), once ready, with an array containing the original words, their
  33. * spelling status and optionally an array of suggestions.
  34. * @param {string=} opt_language Content language.
  35. * @constructor
  36. * @extends {goog.events.EventTarget}
  37. * @final
  38. */
  39. goog.spell.SpellCheck = function(opt_lookupFunction, opt_language) {
  40. goog.events.EventTarget.call(this);
  41. /**
  42. * Function used to lookup spelling of words.
  43. * @type {Function}
  44. * @private
  45. */
  46. this.lookupFunction_ = opt_lookupFunction || null;
  47. /**
  48. * Cache for words not yet checked with lookup function.
  49. * @type {goog.structs.Set}
  50. * @private
  51. */
  52. this.unknownWords_ = new goog.structs.Set();
  53. this.setLanguage(opt_language);
  54. };
  55. goog.inherits(goog.spell.SpellCheck, goog.events.EventTarget);
  56. /**
  57. * Delay, in ms, to wait for additional words to be entered before a lookup
  58. * operation is triggered.
  59. *
  60. * @type {number}
  61. * @private
  62. */
  63. goog.spell.SpellCheck.LOOKUP_DELAY_ = 100;
  64. /**
  65. * Constants for event names
  66. *
  67. * @enum {string}
  68. */
  69. goog.spell.SpellCheck.EventType = {
  70. /**
  71. * Fired when all pending words have been processed.
  72. */
  73. READY: 'ready',
  74. /**
  75. * Fired when all lookup function failed.
  76. */
  77. ERROR: 'error',
  78. /**
  79. * Fired when a word's status is changed.
  80. */
  81. WORD_CHANGED: 'wordchanged'
  82. };
  83. /**
  84. * Cache. Shared across all spell checker instances. Map with langauge as the
  85. * key and a cache for that language as the value.
  86. *
  87. * @type {Object}
  88. * @private
  89. */
  90. goog.spell.SpellCheck.cache_ = {};
  91. /**
  92. * Content Language.
  93. * @type {string}
  94. * @private
  95. */
  96. goog.spell.SpellCheck.prototype.language_ = '';
  97. /**
  98. * Cache for set language. Reference to the element corresponding to the set
  99. * language in the static goog.spell.SpellCheck.cache_.
  100. *
  101. * @type {Object|undefined}
  102. * @private
  103. */
  104. goog.spell.SpellCheck.prototype.cache_;
  105. /**
  106. * Id for timer processing the pending queue.
  107. *
  108. * @type {number}
  109. * @private
  110. */
  111. goog.spell.SpellCheck.prototype.queueTimer_ = 0;
  112. /**
  113. * Whether a lookup operation is in progress.
  114. *
  115. * @type {boolean}
  116. * @private
  117. */
  118. goog.spell.SpellCheck.prototype.lookupInProgress_ = false;
  119. /**
  120. * Codes representing the status of an individual word.
  121. *
  122. * @enum {number}
  123. */
  124. goog.spell.SpellCheck.WordStatus = {
  125. UNKNOWN: 0,
  126. VALID: 1,
  127. INVALID: 2,
  128. IGNORED: 3,
  129. CORRECTED: 4 // Temporary status, not stored in cache
  130. };
  131. /**
  132. * Fields for word array in cache.
  133. *
  134. * @enum {number}
  135. */
  136. goog.spell.SpellCheck.CacheIndex = {
  137. STATUS: 0,
  138. SUGGESTIONS: 1
  139. };
  140. /**
  141. * Regular expression for identifying word boundaries.
  142. *
  143. * @type {string}
  144. */
  145. goog.spell.SpellCheck.WORD_BOUNDARY_CHARS =
  146. '\t\r\n\u00A0 !\"#$%&()*+,\-.\/:;<=>?@\[\\\]^_`{|}~';
  147. /**
  148. * Regular expression for identifying word boundaries.
  149. *
  150. * @type {RegExp}
  151. */
  152. goog.spell.SpellCheck.WORD_BOUNDARY_REGEX =
  153. new RegExp('[' + goog.spell.SpellCheck.WORD_BOUNDARY_CHARS + ']');
  154. /**
  155. * Regular expression for splitting a string into individual words and blocks of
  156. * separators. Matches zero or one word followed by zero or more separators.
  157. *
  158. * @type {RegExp}
  159. */
  160. goog.spell.SpellCheck.SPLIT_REGEX = new RegExp(
  161. '([^' + goog.spell.SpellCheck.WORD_BOUNDARY_CHARS + ']*)' +
  162. '([' + goog.spell.SpellCheck.WORD_BOUNDARY_CHARS + ']*)');
  163. /**
  164. * Sets the lookup function.
  165. *
  166. * @param {Function} f Function to use for word lookup. Must accept an array of
  167. * words, an object reference and a callback function as parameters.
  168. * It must also call the callback function (as a method on the object),
  169. * once ready, with an array containing the original words, their
  170. * spelling status and optionally an array of suggestions.
  171. */
  172. goog.spell.SpellCheck.prototype.setLookupFunction = function(f) {
  173. this.lookupFunction_ = f;
  174. };
  175. /**
  176. * Sets language.
  177. *
  178. * @param {string=} opt_language Content language.
  179. */
  180. goog.spell.SpellCheck.prototype.setLanguage = function(opt_language) {
  181. this.language_ = opt_language || '';
  182. if (!goog.spell.SpellCheck.cache_[this.language_]) {
  183. goog.spell.SpellCheck.cache_[this.language_] = {};
  184. }
  185. this.cache_ = goog.spell.SpellCheck.cache_[this.language_];
  186. };
  187. /**
  188. * Returns language.
  189. *
  190. * @return {string} Content language.
  191. */
  192. goog.spell.SpellCheck.prototype.getLanguage = function() {
  193. return this.language_;
  194. };
  195. /**
  196. * Checks spelling for a block of text.
  197. *
  198. * @param {string} text Block of text to spell check.
  199. */
  200. goog.spell.SpellCheck.prototype.checkBlock = function(text) {
  201. var words = text.split(goog.spell.SpellCheck.WORD_BOUNDARY_REGEX);
  202. var len = words.length;
  203. for (var word, i = 0; i < len; i++) {
  204. word = words[i];
  205. this.checkWord_(word);
  206. }
  207. if (!this.queueTimer_ && !this.lookupInProgress_ &&
  208. this.unknownWords_.getCount()) {
  209. this.processPending_();
  210. } else if (this.unknownWords_.getCount() == 0) {
  211. this.dispatchEvent(goog.spell.SpellCheck.EventType.READY);
  212. }
  213. };
  214. /**
  215. * Checks spelling for a single word. Returns the status of the supplied word,
  216. * or UNKNOWN if it's not cached. If it's not cached the word is added to a
  217. * queue and checked with the verification implementation with a short delay.
  218. *
  219. * @param {string} word Word to check spelling of.
  220. * @return {goog.spell.SpellCheck.WordStatus} The status of the supplied word,
  221. * or UNKNOWN if it's not cached.
  222. */
  223. goog.spell.SpellCheck.prototype.checkWord = function(word) {
  224. var status = this.checkWord_(word);
  225. if (status == goog.spell.SpellCheck.WordStatus.UNKNOWN && !this.queueTimer_ &&
  226. !this.lookupInProgress_) {
  227. this.queueTimer_ = goog.Timer.callOnce(
  228. this.processPending_, goog.spell.SpellCheck.LOOKUP_DELAY_, this);
  229. }
  230. return status;
  231. };
  232. /**
  233. * Checks spelling for a single word. Returns the status of the supplied word,
  234. * or UNKNOWN if it's not cached.
  235. *
  236. * @param {string} word Word to check spelling of.
  237. * @return {goog.spell.SpellCheck.WordStatus} The status of the supplied word,
  238. * or UNKNOWN if it's not cached.
  239. * @private
  240. */
  241. goog.spell.SpellCheck.prototype.checkWord_ = function(word) {
  242. if (!word) {
  243. return goog.spell.SpellCheck.WordStatus.INVALID;
  244. }
  245. var cacheEntry = this.cache_[word];
  246. if (!cacheEntry) {
  247. this.unknownWords_.add(word);
  248. return goog.spell.SpellCheck.WordStatus.UNKNOWN;
  249. }
  250. return cacheEntry[goog.spell.SpellCheck.CacheIndex.STATUS];
  251. };
  252. /**
  253. * Processes pending words unless a lookup operation has already been queued or
  254. * is in progress.
  255. *
  256. * @throws {Error}
  257. */
  258. goog.spell.SpellCheck.prototype.processPending = function() {
  259. if (this.unknownWords_.getCount()) {
  260. if (!this.queueTimer_ && !this.lookupInProgress_) {
  261. this.processPending_();
  262. }
  263. } else {
  264. this.dispatchEvent(goog.spell.SpellCheck.EventType.READY);
  265. }
  266. };
  267. /**
  268. * Processes pending words using the verification callback.
  269. *
  270. * @throws {Error}
  271. * @private
  272. */
  273. goog.spell.SpellCheck.prototype.processPending_ = function() {
  274. if (!this.lookupFunction_) {
  275. throw Error('No lookup function provided for spell checker.');
  276. }
  277. if (this.unknownWords_.getCount()) {
  278. this.lookupInProgress_ = true;
  279. var func = this.lookupFunction_;
  280. func(this.unknownWords_.getValues(), this, this.lookupCallback_);
  281. } else {
  282. this.dispatchEvent(goog.spell.SpellCheck.EventType.READY);
  283. }
  284. this.queueTimer_ = 0;
  285. };
  286. /**
  287. * Callback for lookup function.
  288. *
  289. * @param {Array<Array<?>>} data Data array. Each word is represented by an
  290. * array containing the word, the status and optionally an array of
  291. * suggestions. Passing null indicates that the operation failed.
  292. * @private
  293. *
  294. * Example:
  295. * obj.lookupCallback_([
  296. * ['word', VALID],
  297. * ['wrod', INVALID, ['word', 'wood', 'rod']]
  298. * ]);
  299. */
  300. goog.spell.SpellCheck.prototype.lookupCallback_ = function(data) {
  301. // Lookup function failed; abort then dispatch error event.
  302. if (data == null) {
  303. if (this.queueTimer_) {
  304. goog.Timer.clear(this.queueTimer_);
  305. this.queueTimer_ = 0;
  306. }
  307. this.lookupInProgress_ = false;
  308. this.dispatchEvent(goog.spell.SpellCheck.EventType.ERROR);
  309. return;
  310. }
  311. for (var a, i = 0; a = data[i]; i++) {
  312. this.setWordStatus_(a[0], a[1], a[2]);
  313. }
  314. this.lookupInProgress_ = false;
  315. // Fire ready event if all pending words have been processed.
  316. if (this.unknownWords_.getCount() == 0) {
  317. this.dispatchEvent(goog.spell.SpellCheck.EventType.READY);
  318. // Process pending
  319. } else if (!this.queueTimer_) {
  320. this.queueTimer_ = goog.Timer.callOnce(
  321. this.processPending_, goog.spell.SpellCheck.LOOKUP_DELAY_, this);
  322. }
  323. };
  324. /**
  325. * Sets a words spelling status.
  326. *
  327. * @param {string} word Word to set status for.
  328. * @param {goog.spell.SpellCheck.WordStatus} status Status of word.
  329. * @param {Array<string>=} opt_suggestions Suggestions.
  330. *
  331. * Example:
  332. * obj.setWordStatus('word', VALID);
  333. * obj.setWordStatus('wrod', INVALID, ['word', 'wood', 'rod']);.
  334. */
  335. goog.spell.SpellCheck.prototype.setWordStatus = function(
  336. word, status, opt_suggestions) {
  337. this.setWordStatus_(word, status, opt_suggestions);
  338. };
  339. /**
  340. * Sets a words spelling status.
  341. *
  342. * @param {string} word Word to set status for.
  343. * @param {goog.spell.SpellCheck.WordStatus} status Status of word.
  344. * @param {Array<string>=} opt_suggestions Suggestions.
  345. * @private
  346. */
  347. goog.spell.SpellCheck.prototype.setWordStatus_ = function(
  348. word, status, opt_suggestions) {
  349. var suggestions = opt_suggestions || [];
  350. this.cache_[word] = [status, suggestions];
  351. this.unknownWords_.remove(word);
  352. this.dispatchEvent(
  353. new goog.spell.SpellCheck.WordChangedEvent(this, word, status));
  354. };
  355. /**
  356. * Returns suggestions for the given word.
  357. *
  358. * @param {string} word Word to get suggestions for.
  359. * @return {Array<string>} An array of suggestions for the given word.
  360. */
  361. goog.spell.SpellCheck.prototype.getSuggestions = function(word) {
  362. var cacheEntry = this.cache_[word];
  363. if (!cacheEntry) {
  364. this.checkWord(word);
  365. return [];
  366. }
  367. return cacheEntry[goog.spell.SpellCheck.CacheIndex.STATUS] ==
  368. goog.spell.SpellCheck.WordStatus.INVALID ?
  369. cacheEntry[goog.spell.SpellCheck.CacheIndex.SUGGESTIONS] :
  370. [];
  371. };
  372. /**
  373. * Object representing a word changed event. Fired when the status of a word
  374. * changes.
  375. *
  376. * @param {goog.spell.SpellCheck} target Spellcheck object initiating event.
  377. * @param {string} word Word to set status for.
  378. * @param {goog.spell.SpellCheck.WordStatus} status Status of word.
  379. * @extends {goog.events.Event}
  380. * @constructor
  381. * @final
  382. */
  383. goog.spell.SpellCheck.WordChangedEvent = function(target, word, status) {
  384. goog.events.Event.call(
  385. this, goog.spell.SpellCheck.EventType.WORD_CHANGED, target);
  386. /**
  387. * Word the status has changed for.
  388. * @type {string}
  389. */
  390. this.word = word;
  391. /**
  392. * New status
  393. * @type {goog.spell.SpellCheck.WordStatus}
  394. */
  395. this.status = status;
  396. };
  397. goog.inherits(goog.spell.SpellCheck.WordChangedEvent, goog.events.Event);