1// Copyright 2013 The Chromium Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style license that can be 3// found in the LICENSE file. 4/** 5 * This class stores the filter queries as history and highlight the log text 6 * to increase the readability of the log 7 * 8 * - Enable / Disable highlights 9 * - Highlights text with multiple colors 10 * - Resolve hghlight conficts (A text highlighted by multiple colors) so that 11 * the latest added highlight always has highest priority to display. 12 * 13 */ 14var CrosLogMarker = (function() { 15 'use strict'; 16 17 // Special classes (defined in log_analyzer_view.css) 18 var LOG_MARKER_HIGHLIGHT_CLASS = 'cros-log-analyzer-marker-highlight'; 19 var LOG_MARKER_CONTAINER_ID = 'cros-log-analyzer-marker-container'; 20 var LOG_MARKER_HISTORY_ENTRY_CLASS = 'cros-log-analyzer-marker-history-entry'; 21 var LOG_MARKER_HISTORY_COLOR_TAG_CLASS = 22 'cros-log-analyzer-marker-history-color-tag'; 23 24 /** 25 * Colors used for highlighting. (Current we support 6 colors) 26 * TODO(shinfan): Add more supoorted colors. 27 */ 28 var COLOR_USAGE_SET = { 29 'Crimson': false, 30 'DeepSkyBlue': false, 31 'DarkSeaGreen': false, 32 'GoldenRod': false, 33 'IndianRed': false, 34 'Orange': false 35 }; 36 var COLOR_NUMBER = Object.keys(COLOR_USAGE_SET).length; 37 38 39 /** 40 * CrosHighlightTag represents a single highlight tag in text. 41 */ 42 var CrosHighlightTag = (function() { 43 /** 44 * @constructor 45 */ 46 function CrosHighlightTag(color, field, range, priority) { 47 this.color = color; 48 this.field = field; 49 this.range = range; 50 this.priority = priority; 51 this.enabled = true; 52 } 53 54 return CrosHighlightTag; 55 })(); 56 57 /** 58 * @constructor 59 * @param {CrosLogAnalyzerView} logAnalyzerView A reference to 60 * CrosLogAnalyzerView. 61 */ 62 function CrosLogMarker(logAnalyzerView) { 63 this.container = $(LOG_MARKER_CONTAINER_ID); 64 // Stores highlight objects for each entry. 65 this.entryHighlights = []; 66 // Stores all the filter queries. 67 this.markHistory = {}; 68 // Object references from CrosLogAnalyzerView. 69 this.logEntries = logAnalyzerView.logEntries; 70 this.logAnalyzerView = logAnalyzerView; 71 // Counts how many highlights are created. 72 this.markCount = 0; 73 for (var i = 0; i < this.logEntries.length; i++) { 74 this.entryHighlights.push([]); 75 } 76 } 77 78 CrosLogMarker.prototype = { 79 /** 80 * Saves the query to the mark history and highlights the text 81 * based on the query. 82 */ 83 addMarkHistory: function(query) { 84 // Increases the counter 85 this.markCount += 1; 86 87 // Find an avaiable color. 88 var color = this.pickColor(); 89 if (!color) { 90 // If all colors are occupied. 91 alert('You can only add at most ' + COLOR_NUMBER + 'markers.'); 92 return; 93 } 94 95 // Updates HTML elements. 96 var historyEntry = addNode(this.container, 'div'); 97 historyEntry.className = LOG_MARKER_HISTORY_ENTRY_CLASS; 98 99 // A color tag that indicats the color used. 100 var colorTag = addNode(historyEntry, 'div'); 101 colorTag.className = LOG_MARKER_HISTORY_COLOR_TAG_CLASS; 102 colorTag.style.background = color; 103 104 // Displays the query text. 105 var queryText = addNodeWithText(historyEntry, 'p', query); 106 queryText.style.color = color; 107 108 // Adds a button to remove the marker. 109 var removeBtn = addNodeWithText(historyEntry, 'a', 'Remove'); 110 removeBtn.addEventListener( 111 'click', this.onRemoveBtnClicked_.bind(this, historyEntry, color)); 112 113 // A checkbox that lets user enable and disable the marker. 114 var enableCheckbox = addNode(historyEntry, 'input'); 115 enableCheckbox.type = 'checkbox'; 116 enableCheckbox.checked = true; 117 enableCheckbox.color = color; 118 enableCheckbox.addEventListener('change', 119 this.onEnableCheckboxChange_.bind(this, enableCheckbox), false); 120 121 // Searches log text for matched patterns and highlights them. 122 this.patternMatch(query, color); 123 }, 124 125 /** 126 * Search the text for matched strings 127 */ 128 patternMatch: function(query, color) { 129 var pattern = new RegExp(query, 'i'); 130 for (var i = 0; i < this.logEntries.length; i++) { 131 var entry = this.logEntries[i]; 132 // Search description of each log entry 133 // TODO(shinfan): Add more search fields 134 var positions = this.findPositions( 135 pattern, entry.description); 136 for (var j = 0; j < positions.length; j++) { 137 var pos = positions[j]; 138 this.mark(entry, pos, 'description', color); 139 } 140 this.sortHighlightsByStartPosition_(this.entryHighlights[i]); 141 } 142 }, 143 144 /** 145 * Highlights the text. 146 * @param {CrosLogEntry} entry The log entry to be highlighted 147 * @param {int|Array} position [start, end] 148 * @param {string} field The field of entry to be highlighted 149 * @param {string} color color used for highlighting 150 */ 151 mark: function(entry, position, field, color) { 152 // Creates the highlight object 153 var tag = new CrosHighlightTag(color, field, position, this.markCount); 154 // Add the highlight into entryHighlights 155 this.entryHighlights[entry.rowNum].push(tag); 156 }, 157 158 /** 159 * Find the highlight objects that covers the given position 160 * @param {CrosHighlightTag|Array} highlights highlights of a log entry 161 * @param {int} position The target index 162 * @param {string} field The target field 163 * @return {CrosHighlightTag|Array} Highlights that cover the position 164 */ 165 getHighlight: function(highlights, index, field) { 166 var res = []; 167 for (var j = 0; j < highlights.length; j++) { 168 var highlight = highlights[j]; 169 if (highlight.range[0] <= index && 170 highlight.range[1] > index && 171 highlight.field == field && 172 highlight.enabled) { 173 res.push(highlight); 174 } 175 } 176 /** 177 * Sorts the result by priority so that the highlight with 178 * highest priority comes first. 179 */ 180 this.sortHighlightsByPriority_(res); 181 return res; 182 }, 183 184 /** 185 * This function highlights the entry by going through the text from left 186 * to right and searching for "key" positions. 187 * A "key" position is a position that one (or more) highlight 188 * starts or ends. We only care about "key" positions because this is where 189 * the text highlight status changes. 190 * At each key position, the function decides if the text between this 191 * position and previous position need to be highlighted and resolves 192 * highlight conflicts. 193 * 194 * @param {CrosLogEntry} entry The entry going to be highlighted. 195 * @param {string} field The specified field of the entry. 196 * @param {DOMElement} parent Parent node. 197 */ 198 getHighlightedEntry: function(entry, field, parent) { 199 var rowNum = entry.rowNum; 200 // Get the original text content of the entry (without any highlights). 201 var content = this.logEntries[rowNum][field]; 202 var index = 0; 203 while (index < content.length) { 204 var nextIndex = this.getNextIndex( 205 this.entryHighlights[rowNum], index, field, content); 206 // Searches for highlights that have the position in range. 207 var highlights = this.getHighlight( 208 this.entryHighlights[rowNum], index, field); 209 var text = content.substr(index, nextIndex - index); 210 if (highlights.length > 0) { 211 // Always picks the highlight with highest priority. 212 this.addSpan(text, highlights[0].color, parent); 213 } else { 214 addNodeWithText(parent, 'span', text); 215 } 216 index = nextIndex; 217 } 218 }, 219 220 /** 221 * A helper function that is used by this.getHightlightedEntry 222 * It returns the first index where a highlight begins or ends from 223 * the given index. 224 * @param {CrosHighlightTag|Array} highlights An array of highlights 225 * of a log entry. 226 * @param {int} index The start position. 227 * @param {string} field The specified field of entry. 228 * Other fields are ignored. 229 * @param {string} content The text content of the log entry. 230 * @return {int} The first index where a highlight begins or ends. 231 */ 232 getNextIndex: function(highlights, index, field, content) { 233 var minGap = Infinity; 234 var res = -1; 235 for (var i = 0; i < highlights.length; i++) { 236 if (highlights[i].field != field || !highlights[i].enabled) 237 continue; 238 // Distance between current index and the start index of highlight. 239 var gap1 = highlights[i].range[0] - index; 240 // Distance between current index and the end index of highlight. 241 var gap2 = highlights[i].range[1] - index; 242 if (gap1 > 0 && gap1 < minGap) { 243 minGap = gap1; 244 res = highlights[i].range[0]; 245 } 246 if (gap2 > 0 && gap2 < minGap) { 247 minGap = gap2; 248 res = highlights[i].range[1]; 249 } 250 } 251 // Returns |res| if found. Otherwise returns the end position of the text. 252 return res > 0 ? res : content.length; 253 }, 254 255 /** 256 * A helper function that is used by this.getHightlightedEntry. 257 * It adds the HTML label to the text. 258 */ 259 addSpan: function(text, color, parent) { 260 var span = addNodeWithText(parent, 'span', text); 261 span.style.color = color; 262 span.className = LOG_MARKER_HIGHLIGHT_CLASS; 263 }, 264 265 /** 266 * A helper function that is used by this.getHightlightedEntry. 267 * It adds the HTML label to the text. 268 */ 269 pickColor: function() { 270 for (var color in COLOR_USAGE_SET) { 271 if (!COLOR_USAGE_SET[color]) { 272 COLOR_USAGE_SET[color] = true; 273 return color; 274 } 275 } 276 return false; 277 }, 278 279 /** 280 * A event handler that enables and disables the corresponding marker. 281 * @private 282 */ 283 onEnableCheckboxChange_: function(checkbox) { 284 for (var i = 0; i < this.entryHighlights.length; i++) { 285 for (var j = 0; j < this.entryHighlights[i].length; j++) { 286 if (this.entryHighlights[i][j].color == checkbox.color) { 287 this.entryHighlights[i][j].enabled = checkbox.checked; 288 } 289 } 290 } 291 this.refreshLogTable(); 292 }, 293 294 /** 295 * A event handlier that removes the marker from history. 296 * @private 297 */ 298 onRemoveBtnClicked_: function(entry, color) { 299 entry.parentNode.removeChild(entry); 300 COLOR_USAGE_SET[color] = false; 301 for (var i = 0; i < this.entryHighlights.length; i++) { 302 var highlights = this.entryHighlights[i]; 303 while (true) { 304 var index = this.findHighlightByColor_(highlights, color); 305 if (index == -1) 306 break; 307 highlights.splice(index, 1); 308 } 309 } 310 this.refreshLogTable(); 311 }, 312 313 /** 314 * A helper function that returns the index of first highlight that 315 * has the target color. Otherwise returns -1. 316 * @private 317 */ 318 findHighlightByColor_: function(highlights, color) { 319 for (var i = 0; i < highlights.length; i++) { 320 if (highlights[i].color == color) 321 return i; 322 } 323 return -1; 324 }, 325 326 /** 327 * Refresh the log table in the CrosLogAnalyzerView. 328 */ 329 refreshLogTable: function() { 330 this.logAnalyzerView.populateTable(); 331 this.logAnalyzerView.filterLog(); 332 }, 333 334 /** 335 * A pattern can appear multiple times in a string. 336 * Returns positions of all the appearance. 337 */ 338 findPositions: function(pattern, str) { 339 var res = []; 340 str = str.toLowerCase(); 341 var match = str.match(pattern); 342 if (!match) 343 return res; 344 for (var i = 0; i < match.length; i++) { 345 var index = 0; 346 while (true) { 347 var start = str.indexOf(match[i].toLowerCase(), index); 348 if (start == -1) 349 break; 350 var end = start + match[i].length; 351 res.push([start, end]); 352 index = end + 1; 353 } 354 } 355 return res; 356 }, 357 358 /** 359 * A helper function used in sorting highlights by start position. 360 * @param {HighlightTag} h1, h2 Two highlight tags in the array. 361 * @private 362 */ 363 compareStartPosition_: function(h1, h2) { 364 return h1.range[0] - h2.range[0]; 365 }, 366 367 /** 368 * A helper function used in sorting highlights by priority. 369 * @param {HighlightTag} h1, h2 Two highlight tags in the array. 370 * @private 371 */ 372 comparePriority_: function(h1, h2) { 373 return h2.priority - h1.priority; 374 }, 375 376 /** 377 * A helper function that sorts the highlights array by start position. 378 * @private 379 */ 380 sortHighlightsByStartPosition_: function(highlights) { 381 highlights.sort(this.compareStartPosition_); 382 }, 383 384 /** 385 * A helper function that sorts the highlights array by priority. 386 * @private 387 */ 388 sortHighlightsByPriority_: function(highlights) { 389 highlights.sort(this.comparePriority_); 390 } 391 }; 392 393 return CrosLogMarker; 394})(); 395