| @@ -1,8 +1,99 @@ |
| | 1 | # |
| | 2 | # Match functions |
| | 3 | # |
| | 4 | |
| | 5 | SEPARATOR = '.' |
| | 6 | |
| | 7 | query = |
| | 8 | queryLength = |
| | 9 | value = |
| | 10 | valueLength = |
| | 11 | matcher = # current match function |
| | 12 | fuzzyRegexp = # query fuzzy regexp |
| | 13 | index = # position of the query in the string being matched |
| | 14 | lastIndex = # last position of the query in the string being matched |
| | 15 | match = # regexp match data |
| | 16 | score = # score for the current match |
| | 17 | separators = # counter |
| | 18 | i = null # cursor |
| | 19 | |
| | 20 | `function exactMatch() {` |
| | 21 | index = value.indexOf(query) |
| | 22 | return unless index >= 0 |
| | 23 | |
| | 24 | lastIndex = value.lastIndexOf(query) |
| | 25 | |
| | 26 | if index isnt lastIndex |
| | 27 | return Math.max(scoreExactMatch(), (index = lastIndex) and scoreExactMatch()) |
| | 28 | else |
| | 29 | return scoreExactMatch() |
| | 30 | `}` |
| | 31 | |
| | 32 | `function scoreExactMatch() {` |
| | 33 | # Remove one point for each unmatched character. |
| | 34 | score = 100 - (valueLength - queryLength) |
| | 35 | |
| | 36 | if index > 0 |
| | 37 | # If the character preceding the query is a dot, assign the same score |
| | 38 | # as if the query was found at the beginning of the string, minus one. |
| | 39 | if value.charAt(index - 1) is SEPARATOR |
| | 40 | score += index - 1 |
| | 41 | # Don't match a single-character query unless it's found at the beginning |
| | 42 | # of the string or is preceded by a dot. |
| | 43 | else if queryLength is 1 |
| | 44 | return |
| | 45 | # (1) Remove one point for each unmatched character up to the nearest |
| | 46 | # preceding dot or the beginning of the string. |
| | 47 | # (2) Remove one point for each unmatched character following the query. |
| | 48 | else |
| | 49 | i = index - 2 |
| | 50 | i-- while i >= 0 and value.charAt(i) isnt SEPARATOR |
| | 51 | score -= (index - i) + # (1) |
| | 52 | (valueLength - queryLength - index) # (2) |
| | 53 | |
| | 54 | # Remove one point for each dot preceding the query, except for the one |
| | 55 | # immediately before the query. |
| | 56 | separators = 0 |
| | 57 | i = index - 2 |
| | 58 | while i >= 0 |
| | 59 | separators++ if value.charAt(i) is SEPARATOR |
| | 60 | i-- |
| | 61 | score -= separators |
| | 62 | |
| | 63 | # Remove five points for each dot following the query. |
| | 64 | separators = 0 |
| | 65 | i = valueLength - queryLength - index - 1 |
| | 66 | while i >= 0 |
| | 67 | separators++ if value.charAt(index + queryLength + i) is SEPARATOR |
| | 68 | i-- |
| | 69 | score -= separators * 5 |
| | 70 | |
| | 71 | return Math.max 1, score |
| | 72 | `}` |
| | 73 | |
| | 74 | `function fuzzyMatch() {` |
| | 75 | return if valueLength <= queryLength or value.indexOf(query) >= 0 |
| | 76 | return unless match = fuzzyRegexp.exec(value) |
| | 77 | |
| | 78 | # When the match is at the beginning of the string or preceded by a dot. |
| | 79 | if match.index is 0 or value.charAt(match.index - 1) is SEPARATOR |
| | 80 | return Math.max 66, 100 - match[0].length |
| | 81 | # When the match is at the end of the string. |
| | 82 | else if match.index + match[0].length is valueLength |
| | 83 | return Math.max 33, 67 - match[0].length |
| | 84 | # When the match is in the middle of the string. |
| | 85 | else |
| | 86 | return Math.max 1, 34 - match[0].length |
| | 87 | `}` |
| | 88 | |
| | 89 | # |
| | 90 | # Searchers |
| | 91 | # |
| | 92 | |
| 1 | class app.Searcher | 93 | class app.Searcher |
| 2 | $.extend @prototype, Events | 94 | $.extend @prototype, Events |
| 3 | | 95 | |
| 4 | CHUNK_SIZE = 20000 | 96 | CHUNK_SIZE = 20000 |
| 5 | SEPARATOR = '.' | | |
| 6 | | 97 | |
| 7 | DEFAULTS = | 98 | DEFAULTS = |
| 8 | max_results: app.config.max_results | 99 | max_results: app.config.max_results |
| @@ -11,34 +102,36 @@ class app.Searcher · 2 unmodified lines |
| 11 | constructor: (options = {}) -> | 102 | constructor: (options = {}) -> |
| 12 | @options = $.extend {}, DEFAULTS, options | 103 | @options = $.extend {}, DEFAULTS, options |
| 13 | | 104 | |
| 14 | find: (data, attr, query) -> | 105 | find: (data, attr, q) -> |
| 15 | @kill() | 106 | @kill() |
| 16 | | 107 | |
| 17 | @data = data | 108 | @data = data |
| 18 | @attr = attr | 109 | @attr = attr |
| 19 | @query = query | 110 | @query = q |
| 20 | @setup() | 111 | @setup() |
| 21 | | 112 | |
| 22 | if @isValid() then @match() else @end() | 113 | if @isValid() then @match() else @end() |
| 23 | return | 114 | return |
| 24 | | 115 | |
| 25 | setup: -> | 116 | setup: -> |
| 26 | @query = @normalizeQuery @query | 117 | query = @query = @normalizeQuery(@query) |
| 27 | @queryLength = @query.length | 118 | queryLength = query.length |
| 28 | @dataLength = @data.length | 119 | @dataLength = @data.length |
| 29 | @matchers = ['exactMatch'] | 120 | @matchers = [exactMatch] |
| 30 | @totalResults = 0 | 121 | @totalResults = 0 |
| 31 | @setupFuzzy() | 122 | @setupFuzzy() |
| 32 | return | 123 | return |
| 33 | | 124 | |
| 34 | setupFuzzy: -> | 125 | setupFuzzy: -> |
| 35 | if @queryLength >= @options.fuzzy_min_length | 126 | if queryLength >= @options.fuzzy_min_length |
| 36 | @fuzzyRegexp = @queryToFuzzyRegexp @query | 127 | fuzzyRegexp = @queryToFuzzyRegexp(query) |
| 37 | @matchers.push 'fuzzyMatch' | 128 | @matchers.push(fuzzyMatch) |
| | 129 | else |
| | 130 | fuzzyRegexp = null |
| 38 | return | 131 | return |
| 39 | | 132 | |
| 40 | isValid: -> | 133 | isValid: -> |
| 41 | @queryLength > 0 | 134 | queryLength > 0 |
| 42 | | 135 | |
| 43 | end: -> | 136 | end: -> |
| 44 | @triggerResults [] unless @totalResults | 137 | @triggerResults [] unless @totalResults |
| @@ -53,9 +146,8 @@ class app.Searcher · 8 unmodified lines |
| 53 | return | 146 | return |
| 54 | | 147 | |
| 55 | free: -> | 148 | free: -> |
| 56 | @data = @attr = @query = @queryLength = @dataLength = | 149 | @data = @attr = @dataLength = @matchers = @matcher = @query = |
| 57 | @fuzzyRegexp = @matchers = @totalResults = @scoreMap = | 150 | @totalResults = @scoreMap = @cursor = @timeout = null |
| 58 | @cursor = @matcher = @timeout = null | | |
| 59 | return | 151 | return |
| 60 | | 152 | |
| 61 | match: => | 153 | match: => |
| @@ -82,8 +174,11 @@ class app.Searcher · 20 unmodified lines |
| 82 | return | 174 | return |
| 83 | | 175 | |
| 84 | matchChunk: -> | 176 | matchChunk: -> |
| | 177 | matcher = @matcher |
| 85 | for [0...@chunkSize()] | 178 | for [0...@chunkSize()] |
| 86 | if score = @[@matcher](@data[@cursor][@attr]) | 179 | value = @data[@cursor][@attr] |
| | 180 | valueLength = value.length |
| | 181 | if score = matcher() |
| 87 | @addResult @data[@cursor], score | 182 | @addResult @data[@cursor], score |
| 88 | @cursor++ | 183 | @cursor++ |
| 89 | return | 184 | return |
| @@ -131,83 +226,6 @@ class app.Searcher · 41 unmodified lines |
| 131 | chars[i] = $.escapeRegexp(char) for char, i in chars | 226 | chars[i] = $.escapeRegexp(char) for char, i in chars |
| 132 | new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/ | 227 | new RegExp chars.join('.*?') # abc -> /a.*?b.*?c.*?/ |
| 133 | | 228 | |
| 134 | # | | |
| 135 | # Match functions | | |
| 136 | # | | |
| 137 | | | |
| 138 | index = # position of the query in the string being matched | | |
| 139 | lastIndex = # last position of the query in the string being matched | | |
| 140 | match = # regexp match data | | |
| 141 | score = # score for the current match | | |
| 142 | separators = # counter | | |
| 143 | i = null # cursor | | |
| 144 | | | |
| 145 | exactMatch: (value) -> | | |
| 146 | index = value.indexOf @query | | |
| 147 | return unless index >= 0 | | |
| 148 | | | |
| 149 | lastIndex = value.lastIndexOf @query | | |
| 150 | | | |
| 151 | if index isnt lastIndex | | |
| 152 | Math.max(@scoreExactMatch(value, index), @scoreExactMatch(value, lastIndex)) | | |
| 153 | else | | |
| 154 | @scoreExactMatch(value, index) | | |
| 155 | | | |
| 156 | scoreExactMatch: (value, index) -> | | |
| 157 | # Remove one point for each unmatched character. | | |
| 158 | score = 100 - (value.length - @queryLength) | | |
| 159 | | | |
| 160 | if index > 0 | | |
| 161 | # If the character preceding the query is a dot, assign the same score | | |
| 162 | # as if the query was found at the beginning of the string, minus one. | | |
| 163 | if value[index - 1] is SEPARATOR | | |
| 164 | score += index - 1 | | |
| 165 | # Don't match a single-character query unless it's found at the beginning | | |
| 166 | # of the string or is preceded by a dot. | | |
| 167 | else if @queryLength is 1 | | |
| 168 | return | | |
| 169 | # (1) Remove one point for each unmatched character up to the nearest | | |
| 170 | # preceding dot or the beginning of the string. | | |
| 171 | # (2) Remove one point for each unmatched character following the query. | | |
| 172 | else | | |
| 173 | i = index - 2 | | |
| 174 | i-- while i >= 0 and value[i] isnt SEPARATOR | | |
| 175 | score -= (index - i) + # (1) | | |
| 176 | (value.length - @queryLength - index) # (2) | | |
| 177 | | | |
| 178 | # Remove one point for each dot preceding the query, except for the one | | |
| 179 | # immediately before the query. | | |
| 180 | separators = 0 | | |
| 181 | i = index - 2 | | |
| 182 | while i >= 0 | | |
| 183 | separators++ if value[i] is SEPARATOR | | |
| 184 | i-- | | |
| 185 | score -= separators | | |
| 186 | | | |
| 187 | # Remove five points for each dot following the query. | | |
| 188 | separators = 0 | | |
| 189 | i = value.length - @queryLength - index - 1 | | |
| 190 | while i >= 0 | | |
| 191 | separators++ if value[index + @queryLength + i] is SEPARATOR | | |
| 192 | i-- | | |
| 193 | score -= separators * 5 | | |
| 194 | | | |
| 195 | Math.max 1, score | | |
| 196 | | | |
| 197 | fuzzyMatch: (value) -> | | |
| 198 | return if value.length <= @queryLength or value.indexOf(@query) >= 0 | | |
| 199 | return unless match = @fuzzyRegexp.exec(value) | | |
| 200 | | | |
| 201 | # When the match is at the beginning of the string or preceded by a dot. | | |
| 202 | if match.index is 0 or value[match.index - 1] is SEPARATOR | | |
| 203 | Math.max 66, 100 - match[0].length | | |
| 204 | # When the match is at the end of the string. | | |
| 205 | else if match.index + match[0].length is value.length | | |
| 206 | Math.max 33, 67 - match[0].length | | |
| 207 | # When the match is in the middle of the string. | | |
| 208 | else | | |
| 209 | Math.max 1, 34 - match[0].length | | |
| 210 | | | |
| 211 | class app.SynchronousSearcher extends app.Searcher | 229 | class app.SynchronousSearcher extends app.Searcher |
| 212 | match: => | 230 | match: => |
| 213 | if @matcher | 231 | if @matcher |