blob: 3260db0f0c1381130e18cecb48e5b7012848dfcb [file] [log] [blame]
<!DOCTYPE html>
<!--
Copyright 2015 The Chromium Authors. All rights reserved.
Use of this source code is governed by a BSD-style license that can be
found in the LICENSE file.
-->
<script>
'use strict';
/** Support for autocomplete suggestions. */
var autocomplete = (function() {
// These are characters that are ignored in substring matching.
var UNIMPORTANT_CHAR_REGEX_ = /[ ._-]/;
/**
* This class is used to get autocomplete suggestions. On instantiation,
* this class builds a trie which is used to get matches.
* sourceList have the following properties:
*
* [{name: 'Skydiving'}, {name: 'Snowboarding'}]
*
* With grouping:
* [
* {
* name: 'Outdoor',
* head: true
* },
* {
* name: 'Skydiving',
* group: 'Outdoor'
* }
* ]
* @param {Array.<Object>} sourceList List of object.
* @constructor
*/
var Trie = function(sourceList) {
this.trie_ = [[], {}];
this.headItems_ = {};
this.sourceList_ = sourceList;
this.buildTrie_();
};
/**
* Searches for matching terms and returns their indexes.
* @param {string} target A string to search for.
* @return {Array} List of matched items.
*/
Trie.prototype.search = function(target) {
if (target == null || target == undefined) {
return this.sourceList_;
}
target = target.toLowerCase().trim();
if (target.length == 0) {
return this.sourceList_;
}
var resultDict = {};
// Search full string without splitting on unimportant characters.
this.searchTrie_(target, resultDict);
// Search substring splitting on unimportant characters.
var targetParts = target.split(UNIMPORTANT_CHAR_REGEX_);
if (targetParts.length > 1) {
for (var i = 0; i < targetParts.length; i++) {
this.searchTrie_(targetParts[i], resultDict);
}
}
return this.buildResults(resultDict);
};
/**
* Gets list of result items from resultDict after sorting.
* @param {Object} resultDict Dictionary of source object index to its score.
* @return {Array} List of matched items.
*/
Trie.prototype.buildResults = function(resultDict) {
var resultTuples = [];
for (var index in resultDict) {
resultTuples.push([this.sourceList_[index], resultDict[index]]);
}
var resultList = [];
// If this sourceDatalist contain grouping, include head item in
// results.
if (Object.keys(this.headItems_).length > 0) {
resultTuples.sort(this.compareGroup_);
var groupName = null;
for (var i = 0; i < resultTuples.length; i++) {
var item = resultTuples[i][0];
if (item.group != groupName) {
groupName = item.group;
resultList.push(this.headItems_[groupName]);
}
resultList.push(item);
}
} else {
resultTuples.sort(this.compareScore_);
resultTuples.forEach(function(tuple) {
resultList.push(tuple[0]);
});
}
return resultList;
};
/**
* Searches from the root of the trie for a target string.
* @param {string} target A string to search for.
* @param {Object} resultDict Dictionary of source object index to its score.
*/
Trie.prototype.searchTrie_ = function(target, resultDict) {
var chars = target.split('');
var node = this.trie_;
for (var i = 0; i < chars.length; i++) {
var char = chars[i];
if (char in node[1]) {
node = node[1][char];
} else {
return;
}
}
this.searchAllSubNodes_(node, resultDict, chars.length);
};
/**
* Searches all sub-nodes and sum up scores for source string that are found.
* @param {Object} node A trie node.
* @param {Object} resultDict Dictionary of source object index to its score.
* @param {number} targetLength Length of the target string.
*/
Trie.prototype.searchAllSubNodes_ = function(
node, resultDict, targetLength) {
if (!node) {
return;
}
for (var i = 0; i < node[0].length; i++) {
var data = node[0][i];
if (!(data[0] in resultDict)) {
resultDict[data[0]] = 0;
}
resultDict[data[0]] += this.getScore_(targetLength, data);
}
for (var char in node[1]) {
this.searchAllSubNodes_(node[1][char], resultDict, targetLength);
}
};
/**
* Builds a trie out of the sourceList.
*/
Trie.prototype.buildTrie_ = function() {
for (var i = 0; i < this.sourceList_.length; i++) {
var source = this.sourceList_[i];
if (source.head) {
this.headItems_[source.name] = source;
continue;
}
this.addWord_(this.trie_, source.name, i, source.name.length, 1);
var words = source.name.split(UNIMPORTANT_CHAR_REGEX_);
if (words.length > 1) {
var position = 1;
for (var j = 0; j < words.length; j++) {
this.addWord_(this.trie_, words[j], i, source.name.length, position);
position += words[j].length;
}
}
}
};
/**
* Adds a word to the trie.
* @param {Object} node A trie node.
* @param {string} word A string to add.
* @param {number} index The word's index within the sourceList.
* @param {number} totalLength Total length of a source string.
* @param {number} position Integer position of the word within the source
* string.
*/
Trie.prototype.addWord_ = function(node, word, index, totalLength,
position) {
var characters = word.toLowerCase().split('');
for (var i = 0; i < characters.length; i++) {
var letter = characters[i];
if (!(letter in node[1])) {
node[1][letter] = [[], {}];
}
node = node[1][letter];
}
node[0].push([index, characters.length, totalLength, position]);
};
/**
* Gets the score for a match.
* The more characters matched at the beginning and the shorter the string,
* the higher the score.
* @param {number} targetLength Length of target word.
* @param {Array} data A trie node data.
* @return {number} The score, which is
* target_length * (target_length / source_length)^2 / source_position
*/
Trie.prototype.getScore_ = function(targetLength, data) {
return targetLength * Math.pow(targetLength / data[2], 2) / data[3];
};
/**
* Compares by score.
*/
Trie.prototype.compareScore_ = function(pairA, pairB) {
return pairB[1] - pairA[1];
};
/**
* Compares by group and then by score.
*/
Trie.prototype.compareGroup_ = function(pairA, pairB) {
if (pairA[0].group < pairB[0].group) {
return -1;
}
if (pairA[0].group > pairB[0].group) {
return 1;
}
return pairB[1] - pairA[1];
};
return {
Trie: Trie
};
})();
</script>