Comparing version 1.0.1 to 1.0.2
{ | ||
"name": "typo-js", | ||
"version": "1.0.1", | ||
"version": "1.0.2", | ||
"description": "A Hunspell-style spellchecker.", | ||
@@ -23,3 +23,6 @@ "main": "typo.js", | ||
"homepage": "https://github.com/cfinke/Typo.js#readme", | ||
"tonicExample": "var Typo = require('typo-js'); var dictionary = new Typo('en_US'); dictionary.check('mispelled');" | ||
"tonicExample": "var Typo = require('typo-js'); var dictionary = new Typo('en_US'); dictionary.check('mispelled');", | ||
"browser": { | ||
"fs": false | ||
} | ||
} |
364
typo.js
@@ -1,2 +0,6 @@ | ||
'use strict'; | ||
/* globals chrome: false */ | ||
/* globals __dirname: false */ | ||
/* globals require: false */ | ||
/* globals Buffer: false */ | ||
/* globals module: false */ | ||
@@ -8,2 +12,7 @@ /** | ||
var Typo; | ||
(function () { | ||
"use strict"; | ||
/** | ||
@@ -30,10 +39,14 @@ * Typo constructor. | ||
* {Object} [flags]: flag information. | ||
* {Boolean} [asyncLoad]: If true, affData and wordsData will be loaded | ||
* asynchronously. | ||
* {Function} [loadedCallback]: Called when both affData and wordsData | ||
* have been loaded. Only used if asyncLoad is set to true. The parameter | ||
* is the instantiated Typo object. | ||
* | ||
* | ||
* @returns {Typo} A Typo object. | ||
*/ | ||
var Typo = function (dictionary, affData, wordsData, settings) { | ||
Typo = function (dictionary, affData, wordsData, settings) { | ||
settings = settings || {}; | ||
this.dictionary = null; | ||
@@ -51,49 +64,104 @@ | ||
this.memoized = {}; | ||
this.loaded = false; | ||
var self = this; | ||
var path; | ||
// Loop-control variables. | ||
var i, j, _len, _jlen; | ||
if (dictionary) { | ||
this.dictionary = dictionary; | ||
self.dictionary = dictionary; | ||
if (typeof window !== 'undefined' && 'chrome' in window && 'extension' in window.chrome && 'getURL' in window.chrome.extension) { | ||
if (!affData) affData = this._readFile(chrome.extension.getURL("lib/typo/dictionaries/" + dictionary + "/" + dictionary + ".aff")); | ||
if (!wordsData) wordsData = this._readFile(chrome.extension.getURL("lib/typo/dictionaries/" + dictionary + "/" + dictionary + ".dic")); | ||
} else { | ||
// If the data is preloaded, just setup the Typo object. | ||
if (affData && wordsData) { | ||
setup(); | ||
} | ||
// Loading data for Chrome extentions. | ||
else if (typeof window !== 'undefined' && 'chrome' in window && 'extension' in window.chrome && 'getURL' in window.chrome.extension) { | ||
if (settings.dictionaryPath) { | ||
var path = settings.dictionaryPath; | ||
path = settings.dictionaryPath; | ||
} | ||
else { | ||
path = "typo/dictionaries"; | ||
} | ||
if (!affData) readDataFile(chrome.extension.getURL(path + "/" + dictionary + "/" + dictionary + ".aff"), setAffData); | ||
if (!wordsData) readDataFile(chrome.extension.getURL(path + "/" + dictionary + "/" + dictionary + ".dic"), setWordsData); | ||
} | ||
else { | ||
if (settings.dictionaryPath) { | ||
path = settings.dictionaryPath; | ||
} | ||
else if (typeof __dirname !== 'undefined') { | ||
var path = __dirname + '/dictionaries'; | ||
path = __dirname + '/dictionaries'; | ||
} | ||
else { | ||
var path = './dictionaries'; | ||
path = './dictionaries'; | ||
} | ||
if (!affData) affData = this._readFile(path + "/" + dictionary + "/" + dictionary + ".aff"); | ||
if (!wordsData) wordsData = this._readFile(path + "/" + dictionary + "/" + dictionary + ".dic"); | ||
if (!affData) readDataFile(path + "/" + dictionary + "/" + dictionary + ".aff", setAffData); | ||
if (!wordsData) readDataFile(path + "/" + dictionary + "/" + dictionary + ".dic", setWordsData); | ||
} | ||
} | ||
function readDataFile(url, setFunc) { | ||
var response = self._readFile(url, null, settings.asyncLoad); | ||
this.rules = this._parseAFF(affData); | ||
if (settings.asyncLoad) { | ||
response.then(function(data) { | ||
setFunc(data); | ||
}); | ||
} | ||
else { | ||
setFunc(response); | ||
} | ||
} | ||
function setAffData(data) { | ||
affData = data; | ||
if (wordsData) { | ||
setup(); | ||
} | ||
} | ||
function setWordsData(data) { | ||
wordsData = data; | ||
if (affData) { | ||
setup(); | ||
} | ||
} | ||
function setup() { | ||
self.rules = self._parseAFF(affData); | ||
// Save the rule codes that are used in compound rules. | ||
this.compoundRuleCodes = {}; | ||
self.compoundRuleCodes = {}; | ||
for (var i = 0, _len = this.compoundRules.length; i < _len; i++) { | ||
var rule = this.compoundRules[i]; | ||
for (i = 0, _len = self.compoundRules.length; i < _len; i++) { | ||
var rule = self.compoundRules[i]; | ||
for (var j = 0, _jlen = rule.length; j < _jlen; j++) { | ||
this.compoundRuleCodes[rule[j]] = []; | ||
for (j = 0, _jlen = rule.length; j < _jlen; j++) { | ||
self.compoundRuleCodes[rule[j]] = []; | ||
} | ||
} | ||
// If we add this ONLYINCOMPOUND flag to this.compoundRuleCodes, then _parseDIC | ||
// If we add this ONLYINCOMPOUND flag to self.compoundRuleCodes, then _parseDIC | ||
// will do the work of saving the list of words that are compound-only. | ||
if ("ONLYINCOMPOUND" in this.flags) { | ||
this.compoundRuleCodes[this.flags.ONLYINCOMPOUND] = []; | ||
if ("ONLYINCOMPOUND" in self.flags) { | ||
self.compoundRuleCodes[self.flags.ONLYINCOMPOUND] = []; | ||
} | ||
this.dictionaryTable = this._parseDIC(wordsData); | ||
self.dictionaryTable = self._parseDIC(wordsData); | ||
// Get rid of any codes from the compound rule codes that are never used | ||
// (or that were special regex characters). Not especially necessary... | ||
for (var i in this.compoundRuleCodes) { | ||
if (this.compoundRuleCodes[i].length == 0) { | ||
delete this.compoundRuleCodes[i]; | ||
for (i in self.compoundRuleCodes) { | ||
if (self.compoundRuleCodes[i].length === 0) { | ||
delete self.compoundRuleCodes[i]; | ||
} | ||
@@ -105,12 +173,12 @@ } | ||
// testing for compound words is probably slow. | ||
for (var i = 0, _len = this.compoundRules.length; i < _len; i++) { | ||
var ruleText = this.compoundRules[i]; | ||
for (i = 0, _len = self.compoundRules.length; i < _len; i++) { | ||
var ruleText = self.compoundRules[i]; | ||
var expressionText = ""; | ||
for (var j = 0, _jlen = ruleText.length; j < _jlen; j++) { | ||
for (j = 0, _jlen = ruleText.length; j < _jlen; j++) { | ||
var character = ruleText[j]; | ||
if (character in this.compoundRuleCodes) { | ||
expressionText += "(" + this.compoundRuleCodes[character].join("|") + ")"; | ||
if (character in self.compoundRuleCodes) { | ||
expressionText += "(" + self.compoundRuleCodes[character].join("|") + ")"; | ||
} | ||
@@ -122,4 +190,10 @@ else { | ||
this.compoundRules[i] = new RegExp(expressionText, "i"); | ||
self.compoundRules[i] = new RegExp(expressionText, "i"); | ||
} | ||
self.loaded = true; | ||
if (settings.asyncLoad && settings.loadedCallback) { | ||
settings.loadedCallback(self); | ||
} | ||
} | ||
@@ -139,3 +213,5 @@ | ||
for (var i in obj) { | ||
this[i] = obj[i]; | ||
if (obj.hasOwnProperty(i)) { | ||
this[i] = obj[i]; | ||
} | ||
} | ||
@@ -151,11 +227,32 @@ | ||
* @param {String} [charset="ISO8859-1"] The expected charset of the file | ||
* @returns string The file data. | ||
* @param {Boolean} async If true, the file will be read asynchronously. For node.js this does nothing, all | ||
* files are read synchronously. | ||
* @returns {String} The file data if async is false, otherwise a promise object. If running node.js, the data is | ||
* always returned. | ||
*/ | ||
_readFile : function (path, charset) { | ||
if (!charset) charset = "utf8"; | ||
_readFile : function (path, charset, async) { | ||
charset = charset || "utf8"; | ||
if (typeof XMLHttpRequest !== 'undefined') { | ||
var promise; | ||
var req = new XMLHttpRequest(); | ||
req.open("GET", path, false); | ||
req.open("GET", path, async); | ||
if (async) { | ||
promise = new Promise(function(resolve, reject) { | ||
req.onload = function() { | ||
if (req.status === 200) { | ||
resolve(req.responseText); | ||
} | ||
else { | ||
reject(req.statusText); | ||
} | ||
}; | ||
req.onerror = function() { | ||
reject(req.statusText); | ||
} | ||
}); | ||
} | ||
@@ -167,3 +264,3 @@ if (req.overrideMimeType) | ||
return req.responseText; | ||
return async ? promise : req.responseText; | ||
} | ||
@@ -206,2 +303,5 @@ else if (typeof require !== 'undefined') { | ||
var line, subline, numEntries, lineParts; | ||
var i, j, _len, _jlen; | ||
// Remove comment lines | ||
@@ -212,4 +312,4 @@ data = this._removeAffixComments(data); | ||
for (var i = 0, _len = lines.length; i < _len; i++) { | ||
var line = lines[i]; | ||
for (i = 0, _len = lines.length; i < _len; i++) { | ||
line = lines[i]; | ||
@@ -223,10 +323,10 @@ var definitionParts = line.split(/\s+/); | ||
var combineable = definitionParts[2]; | ||
var numEntries = parseInt(definitionParts[3], 10); | ||
numEntries = parseInt(definitionParts[3], 10); | ||
var entries = []; | ||
for (var j = i + 1, _jlen = i + 1 + numEntries; j < _jlen; j++) { | ||
var line = lines[j]; | ||
for (j = i + 1, _jlen = i + 1 + numEntries; j < _jlen; j++) { | ||
subline = lines[j]; | ||
var lineParts = line.split(/\s+/); | ||
lineParts = subline.split(/\s+/); | ||
var charactersToRemove = lineParts[2]; | ||
@@ -274,8 +374,8 @@ | ||
else if (ruleType === "COMPOUNDRULE") { | ||
var numEntries = parseInt(definitionParts[1], 10); | ||
numEntries = parseInt(definitionParts[1], 10); | ||
for (var j = i + 1, _jlen = i + 1 + numEntries; j < _jlen; j++) { | ||
var line = lines[j]; | ||
for (j = i + 1, _jlen = i + 1 + numEntries; j < _jlen; j++) { | ||
line = lines[j]; | ||
var lineParts = line.split(/\s+/); | ||
lineParts = line.split(/\s+/); | ||
this.compoundRules.push(lineParts[1]); | ||
@@ -287,3 +387,3 @@ } | ||
else if (ruleType === "REP") { | ||
var lineParts = line.split(/\s+/); | ||
lineParts = line.split(/\s+/); | ||
@@ -347,7 +447,13 @@ if (lineParts.length === 3) { | ||
// Some dictionaries will list the same word multiple times with different rule sets. | ||
if (!(word in dictionaryTable) || typeof dictionaryTable[word] != 'object') { | ||
dictionaryTable[word] = []; | ||
if (!dictionaryTable.hasOwnProperty(word)) { | ||
dictionaryTable[word] = null; | ||
} | ||
dictionaryTable[word].push(rules); | ||
if (rules.length > 0) { | ||
if (dictionaryTable[word] === null) { | ||
dictionaryTable[word] = []; | ||
} | ||
dictionaryTable[word].push(rules); | ||
} | ||
} | ||
@@ -454,3 +560,3 @@ | ||
else if (this.flags.FLAG === "num") { | ||
return textCode.split(","); | ||
return textCodes.split(","); | ||
} | ||
@@ -524,2 +630,6 @@ }, | ||
check : function (aWord) { | ||
if (!this.loaded) { | ||
throw "Dictionary not loaded."; | ||
} | ||
// Remove leading and trailing whitespace | ||
@@ -573,8 +683,14 @@ var trimmedWord = aWord.replace(/^\s\s*/, '').replace(/\s\s*$/, ''); | ||
checkExact : function (word) { | ||
if (!this.loaded) { | ||
throw "Dictionary not loaded."; | ||
} | ||
var ruleCodes = this.dictionaryTable[word]; | ||
var i, _len; | ||
if (typeof ruleCodes === 'undefined') { | ||
// Check if this might be a compound word. | ||
if ("COMPOUNDMIN" in this.flags && word.length >= this.flags.COMPOUNDMIN) { | ||
for (var i = 0, _len = this.compoundRules.length; i < _len; i++) { | ||
for (i = 0, _len = this.compoundRules.length; i < _len; i++) { | ||
if (word.match(this.compoundRules[i])) { | ||
@@ -585,7 +701,10 @@ return true; | ||
} | ||
return false; | ||
} | ||
else if (ruleCodes === null) { | ||
// a null (but not undefined) value for an entry in the dictionary table | ||
// means that the word is in the dictionary but has no flags. | ||
return true; | ||
} | ||
else if (typeof ruleCodes === 'object') { // this.dictionary['hasOwnProperty'] will be a function. | ||
for (var i = 0, _len = ruleCodes.length; i < _len; i++) { | ||
for (i = 0, _len = ruleCodes.length; i < _len; i++) { | ||
if (!this.hasFlag(word, "ONLYINCOMPOUND", ruleCodes[i])) { | ||
@@ -595,5 +714,5 @@ return true; | ||
} | ||
return false; | ||
} | ||
return false; | ||
}, | ||
@@ -610,5 +729,9 @@ | ||
hasFlag : function (word, flag, wordFlags) { | ||
if (!this.loaded) { | ||
throw "Dictionary not loaded."; | ||
} | ||
if (flag in this.flags) { | ||
if (typeof wordFlags === 'undefined') { | ||
var wordFlags = Array.prototype.concat.apply([], this.dictionaryTable[word]); | ||
wordFlags = Array.prototype.concat.apply([], this.dictionaryTable[word]); | ||
} | ||
@@ -638,3 +761,17 @@ | ||
suggest : function (word, limit) { | ||
if (!limit) limit = 5; | ||
if (!this.loaded) { | ||
throw "Dictionary not loaded."; | ||
} | ||
limit = limit || 5; | ||
if (this.memoized.hasOwnProperty(word)) { | ||
var memoizedLimit = this.memoized[word]['limit']; | ||
// Only return the cached list if it's big enough or if there weren't enough suggestions | ||
// to fill a smaller limit. | ||
if (limit <= memoizedLimit || this.memoized[word]['suggestions'].length < memoizedLimit) { | ||
return this.memoized[word]['suggestions'].slice(0, limit); | ||
} | ||
} | ||
@@ -683,59 +820,34 @@ if (this.check(word)) return []; | ||
for (var ii = 0, _iilen = words.length; ii < _iilen; ii++) { | ||
var ii, i, j, _iilen, _len, _jlen; | ||
for (ii = 0, _iilen = words.length; ii < _iilen; ii++) { | ||
var word = words[ii]; | ||
var splits = []; | ||
for (var i = 0, _len = word.length + 1; i < _len; i++) { | ||
splits.push([ word.substring(0, i), word.substring(i, word.length) ]); | ||
} | ||
var deletes = []; | ||
for (var i = 0, _len = splits.length; i < _len; i++) { | ||
var s = splits[i]; | ||
for (i = 0, _len = word.length + 1; i < _len; i++) { | ||
var s = [ word.substring(0, i), word.substring(i) ]; | ||
if (s[1]) { | ||
deletes.push(s[0] + s[1].substring(1)); | ||
rv.push(s[0] + s[1].substring(1)); | ||
} | ||
} | ||
var transposes = []; | ||
for (var i = 0, _len = splits.length; i < _len; i++) { | ||
var s = splits[i]; | ||
if (s[1].length > 1) { | ||
transposes.push(s[0] + s[1][1] + s[1][0] + s[1].substring(2)); | ||
// Eliminate transpositions of identical letters | ||
if (s[1].length > 1 && s[1][1] !== s[1][0]) { | ||
rv.push(s[0] + s[1][1] + s[1][0] + s[1].substring(2)); | ||
} | ||
} | ||
var replaces = []; | ||
for (var i = 0, _len = splits.length; i < _len; i++) { | ||
var s = splits[i]; | ||
if (s[1]) { | ||
for (var j = 0, _jlen = self.alphabet.length; j < _jlen; j++) { | ||
replaces.push(s[0] + self.alphabet[j] + s[1].substring(1)); | ||
for (j = 0, _jlen = self.alphabet.length; j < _jlen; j++) { | ||
// Eliminate replacement of a letter by itself | ||
if (self.alphabet[j] != s[1].substring(0,1)){ | ||
rv.push(s[0] + self.alphabet[j] + s[1].substring(1)); | ||
} | ||
} | ||
} | ||
} | ||
var inserts = []; | ||
for (var i = 0, _len = splits.length; i < _len; i++) { | ||
var s = splits[i]; | ||
if (s[1]) { | ||
for (var j = 0, _jlen = self.alphabet.length; j < _jlen; j++) { | ||
replaces.push(s[0] + self.alphabet[j] + s[1]); | ||
for (j = 0, _jlen = self.alphabet.length; j < _jlen; j++) { | ||
rv.push(s[0] + self.alphabet[j] + s[1]); | ||
} | ||
} | ||
} | ||
rv = rv.concat(deletes); | ||
rv = rv.concat(transposes); | ||
rv = rv.concat(replaces); | ||
rv = rv.concat(inserts); | ||
} | ||
@@ -749,3 +861,3 @@ | ||
for (var i = 0; i < words.length; i++) { | ||
for (var i = 0, _len = words.length; i < _len; i++) { | ||
if (self.check(words[i])) { | ||
@@ -764,8 +876,10 @@ rv.push(words[i]); | ||
var corrections = known(ed1).concat(known(ed2)); | ||
var corrections = known(ed1.concat(ed2)); | ||
var i, _len; | ||
// Sort the edits based on how many different ways they were created. | ||
var weighted_corrections = {}; | ||
for (var i = 0, _len = corrections.length; i < _len; i++) { | ||
for (i = 0, _len = corrections.length; i < _len; i++) { | ||
if (!(corrections[i] in weighted_corrections)) { | ||
@@ -781,4 +895,6 @@ weighted_corrections[corrections[i]] = 1; | ||
for (var i in weighted_corrections) { | ||
sorted_corrections.push([ i, weighted_corrections[i] ]); | ||
for (i in weighted_corrections) { | ||
if (weighted_corrections.hasOwnProperty(i)) { | ||
sorted_corrections.push([ i, weighted_corrections[i] ]); | ||
} | ||
} | ||
@@ -797,4 +913,20 @@ | ||
var rv = []; | ||
var capitalization_scheme = "lowercase"; | ||
for (var i = 0, _len = Math.min(limit, sorted_corrections.length); i < _len; i++) { | ||
if (word.toUpperCase() === word) { | ||
capitalization_scheme = "uppercase"; | ||
} | ||
else if (word.substr(0, 1).toUpperCase() + word.substr(1).toLowerCase() === word) { | ||
capitalization_scheme = "capitalized"; | ||
} | ||
for (i = 0, _len = Math.min(limit, sorted_corrections.length); i < _len; i++) { | ||
if ("uppercase" === capitalization_scheme) { | ||
sorted_corrections[i][0] = sorted_corrections[i][0].toUpperCase(); | ||
} | ||
else if ("capitalized" === capitalization_scheme) { | ||
sorted_corrections[i][0] = sorted_corrections[i][0].substr(0, 1).toUpperCase() + sorted_corrections[i][0].substr(1); | ||
} | ||
if (!self.hasFlag(sorted_corrections[i][0], "NOSUGGEST")) { | ||
@@ -808,5 +940,11 @@ rv.push(sorted_corrections[i][0]); | ||
return correct(word); | ||
this.memoized[word] = { | ||
'suggestions': correct(word), | ||
'limit': limit | ||
}; | ||
return this.memoized[word]['suggestions']; | ||
} | ||
}; | ||
})(); | ||
@@ -813,0 +951,0 @@ // Support for use as a node.js module. |
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
License Policy Violation
LicenseThis package is not allowed per your license policy. Review the package's license to ensure compliance.
Found 1 instance in 1 package
725520
727