# # Match functions # SEPARATOR = '.' query = queryLength = value = valueLength = matcher = # current match function fuzzyRegexp = # query fuzzy regexp index = # position of the query in the string being matched lastIndex = # last position of the query in the string being matched match = # regexp match data matchIndex = matchLength = score = # score for the current match separators = # counter i = null # cursor `function exactMatch() {` index = value.indexOf(query) return unless index >= 0 lastIndex = value.lastIndexOf(query) if index isnt lastIndex return Math.max(scoreExactMatch(), (index = lastIndex) and scoreExactMatch()) else return scoreExactMatch() `}` `function scoreExactMatch() {` # Remove one point for each unmatched character. score = 100 - (valueLength - queryLength) if index > 0 # If the character preceding the query is a dot, assign the same score # as if the query was found at the beginning of the string, minus one. if value.charAt(index - 1) is SEPARATOR score += index - 1 # Don't match a single-character query unless it's found at the beginning # of the string or is preceded by a dot. else if queryLength is 1 return # (1) Remove one point for each unmatched character up to the nearest # preceding dot or the beginning of the string. # (2) Remove one point for each unmatched character following the query. else i = index - 2 i-- while i >= 0 and value.charAt(i) isnt SEPARATOR score -= (index - i) + # (1) (valueLength - queryLength - index) # (2) # Remove one point for each dot preceding the query, except for the one # immediately before the query. separators = 0 i = index - 2 while i >= 0 separators++ if value.charAt(i) is SEPARATOR i-- score -= separators # Remove five points for each dot following the query. separators = 0 i = valueLength - queryLength - index - 1 while i >= 0 separators++ if value.charAt(index + queryLength + i) is SEPARATOR i-- score -= separators * 5 return Math.max 1, score `}` `function fuzzyMatch() {` return if valueLength <= queryLength or value.indexOf(query) >= 0 return unless match = fuzzyRegexp.exec(value) matchIndex = match.index matchLength = match[0].length score = scoreFuzzyMatch() if match = fuzzyRegexp.exec(value.slice(i = value.lastIndexOf(SEPARATOR) + 1)) matchIndex = i + match.index matchLength = match[0].length return Math.max(score, scoreFuzzyMatch()) else return score `}` `function scoreFuzzyMatch() {` # When the match is at the beginning of the string or preceded by a dot. if matchIndex is 0 or value.charAt(matchIndex - 1) is SEPARATOR return Math.max 66, 100 - matchLength # When the match is at the end of the string. else if matchIndex + matchLength is valueLength return Math.max 33, 67 - matchLength # When the match is in the middle of the string. else return Math.max 1, 34 - matchLength `}` # # Searchers # class app.Searcher $.extend @prototype, Events CHUNK_SIZE = 20000 DEFAULTS = max_results: app.config.max_results fuzzy_min_length: 3 SEPARATORS_REGEXP = /\:?\ |#|::|->/g PARANTHESES_REGEXP = /\(.*?\)$/ EVENT_REGEXP = /\ event$/ DOT_REGEXP = /\.+/g WHITESPACE_REGEXP = /\s/g EMPTY_STRING = '' ELLIPSIS = '...' @normalizeString: (string) -> string .toLowerCase() .replace ELLIPSIS, EMPTY_STRING .replace EVENT_REGEXP, EMPTY_STRING .replace SEPARATORS_REGEXP, SEPARATOR .replace DOT_REGEXP, SEPARATOR .replace PARANTHESES_REGEXP, EMPTY_STRING .replace WHITESPACE_REGEXP, EMPTY_STRING constructor: (options = {}) -> @options = $.extend {}, DEFAULTS, options find: (data, attr, q) -> @kill() @data = data @attr = attr @query = q @setup() if @isValid() then @match() else @end() return setup: -> query = @query = @constructor.normalizeString(@query) queryLength = query.length @dataLength = @data.length @matchers = [exactMatch] @totalResults = 0 @setupFuzzy() return setupFuzzy: -> if queryLength >= @options.fuzzy_min_length fuzzyRegexp = @queryToFuzzyRegexp(query) @matchers.push(fuzzyMatch) else fuzzyRegexp = null return isValid: -> queryLength > 0 end: -> @triggerResults [] unless @totalResults @trigger 'end' @free() return kill: -> if @timeout clearTimeout @timeout @free() return free: -> @data = @attr = @dataLength = @matchers = @matcher = @query = @totalResults = @scoreMap = @cursor = @timeout = null return match: => if not @foundEnough() and @matcher = @matchers.shift() @setupMatcher() @matchChunks() else @end() return setupMatcher: -> @cursor = 0 @scoreMap = new Array(101) return matchChunks: => @matchChunk() if @cursor is @dataLength or @scoredEnough() @delay @match @sendResults() else @delay @matchChunks return matchChunk: -> matcher = @matcher for [0...@chunkSize()] value = @data[@cursor][@attr] valueLength = value.length if score = matcher() @addResult @data[@cursor], score @cursor++ return chunkSize: -> if @cursor + CHUNK_SIZE > @dataLength @dataLength % CHUNK_SIZE else CHUNK_SIZE scoredEnough: -> @scoreMap[100]?.length >= @options.max_results foundEnough: -> @totalResults >= @options.max_results addResult: (object, score) -> (@scoreMap[Math.round(score)] or= []).push(object) @totalResults++ return getResults: -> results = [] for objects in @scoreMap by -1 when objects results.push.apply results, objects results[0...@options.max_results] sendResults: -> results = @getResults() @triggerResults results if results.length return triggerResults: (results) -> @trigger 'results', results return delay: (fn) -> @timeout = setTimeout(fn, 1) queryToFuzzyRegexp: (string) -> chars = string.split '' chars[i] = $.escapeRegexp(char) for char, i in chars new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/ class app.SynchronousSearcher extends app.Searcher match: => if @matcher @allResults or= [] @allResults.push.apply @allResults, @getResults() super free: -> @allResults = null super end: -> @sendResults true super sendResults: (end) -> if end and @allResults?.length @triggerResults @allResults delay: (fn) -> fn()