blob: d67a00c8dfbe1152323aa6009a59897fbc8c2b94 [file] [log] [blame]
// Copyright 2013 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.
/**
* This class stores the filter queries as history and highlight the log text
* to increase the readability of the log
*
* - Enable / Disable highlights
* - Highlights text with multiple colors
* - Resolve hghlight conficts (A text highlighted by multiple colors) so that
* the latest added highlight always has highest priority to display.
*
*/
var CrosLogMarker = (function() {
'use strict';
// Special classes (defined in log_visualizer_view.css)
var LOG_MARKER_HIGHLIGHT_CLASS = 'cros-log-visualizer-marker-highlight';
var LOG_MARKER_CONTAINER_ID = 'cros-log-visualizer-marker-container';
var LOG_MARKER_HISTORY_ENTRY_CLASS =
'cros-log-visualizer-marker-history-entry';
var LOG_MARKER_HISTORY_COLOR_TAG_CLASS =
'cros-log-visualizer-marker-history-color-tag';
/**
* Colors used for highlighting. (Current we support 6 colors)
* TODO(shinfan): Add more supoorted colors.
*/
var COLOR_USAGE_SET = {
'Crimson': false,
'DeepSkyBlue': false,
'DarkSeaGreen': false,
'GoldenRod': false,
'IndianRed': false,
'Orange': false
};
var COLOR_NUMBER = Object.keys(COLOR_USAGE_SET).length;
/**
* CrosHighlightTag represents a single highlight tag in text.
*/
var CrosHighlightTag = (function() {
/**
* @constructor
*/
function CrosHighlightTag(color, field, range, priority) {
this.color = color;
this.field = field;
this.range = range;
this.priority = priority;
this.enabled = true;
}
return CrosHighlightTag;
})();
/**
* @constructor
* @param {CrosLogVisualizerView} logVisualizerView A reference to
* CrosLogVisualizerView.
*/
function CrosLogMarker(logVisualizerView) {
this.container = $(LOG_MARKER_CONTAINER_ID);
// Stores highlight objects for each entry.
this.entryHighlights = [];
// Stores all the filter queries.
this.markHistory = {};
// Object references from CrosLogVisualizerView.
this.logEntries = logVisualizerView.logEntries;
this.logVisualizerView = logVisualizerView;
// Counts how many highlights are created.
this.markCount = 0;
for (var i = 0; i < this.logEntries.length; i++) {
this.entryHighlights.push([]);
}
}
CrosLogMarker.prototype = {
/**
* Saves the query to the mark history and highlights the text
* based on the query.
*/
addMarkHistory: function(query) {
// Increases the counter
this.markCount += 1;
// Find an avaiable color.
var color = this.pickColor();
if (!color) {
// If all colors are occupied.
alert('You can only add at most ' + COLOR_NUMBER + 'markers.');
return;
}
// Updates HTML elements.
var historyEntry = addNode(this.container, 'div');
historyEntry.className = LOG_MARKER_HISTORY_ENTRY_CLASS;
// A color tag that indicats the color used.
var colorTag = addNode(historyEntry, 'div');
colorTag.className = LOG_MARKER_HISTORY_COLOR_TAG_CLASS;
colorTag.style.background = color;
// Displays the query text.
var queryText = addNodeWithText(historyEntry, 'p', query);
queryText.style.color = color;
// Adds a button to remove the marker.
var removeBtn = addNodeWithText(historyEntry, 'a', 'Remove');
removeBtn.addEventListener(
'click', this.onRemoveBtnClicked_.bind(this, historyEntry, color));
// A checkbox that lets user enable and disable the marker.
var enableCheckbox = addNode(historyEntry, 'input');
enableCheckbox.type = 'checkbox';
enableCheckbox.checked = true;
enableCheckbox.color = color;
enableCheckbox.addEventListener('change',
this.onEnableCheckboxChange_.bind(this, enableCheckbox), false);
// Searches log text for matched patterns and highlights them.
this.patternMatch(query, color);
},
/**
* Search the text for matched strings
*/
patternMatch: function(query, color) {
var pattern = new RegExp(query, 'i');
for (var i = 0; i < this.logEntries.length; i++) {
var entry = this.logEntries[i];
// Search description of each log entry
// TODO(shinfan): Add more search fields
var positions = this.findPositions(
pattern, entry.description);
for (var j = 0; j < positions.length; j++) {
var pos = positions[j];
this.mark(entry, pos, 'description', color);
}
this.sortHighlightsByStartPosition_(this.entryHighlights[i]);
}
},
/**
* Highlights the text.
* @param {CrosLogEntry} entry The log entry to be highlighted
* @param {int|Array} position [start, end]
* @param {string} field The field of entry to be highlighted
* @param {string} color color used for highlighting
*/
mark: function(entry, position, field, color) {
// Creates the highlight object
var tag = new CrosHighlightTag(color, field, position, this.markCount);
// Add the highlight into entryHighlights
this.entryHighlights[entry.rowNum].push(tag);
},
/**
* Find the highlight objects that covers the given position
* @param {CrosHighlightTag|Array} highlights highlights of a log entry
* @param {int} position The target index
* @param {string} field The target field
* @return {CrosHighlightTag|Array} Highlights that cover the position
*/
getHighlight: function(highlights, index, field) {
var res = [];
for (var j = 0; j < highlights.length; j++) {
var highlight = highlights[j];
if (highlight.range[0] <= index &&
highlight.range[1] > index &&
highlight.field == field &&
highlight.enabled) {
res.push(highlight);
}
}
/**
* Sorts the result by priority so that the highlight with
* highest priority comes first.
*/
this.sortHighlightsByPriority_(res);
return res;
},
/**
* This function highlights the entry by going through the text from left
* to right and searching for "key" positions.
* A "key" position is a position that one (or more) highlight
* starts or ends. We only care about "key" positions because this is where
* the text highlight status changes.
* At each key position, the function decides if the text between this
* position and previous position need to be highlighted and resolves
* highlight conflicts.
*
* @param {CrosLogEntry} entry The entry going to be highlighted.
* @param {string} field The specified field of the entry.
* @param {DOMElement} parent Parent node.
*/
getHighlightedEntry: function(entry, field, parent) {
var rowNum = entry.rowNum;
// Get the original text content of the entry (without any highlights).
var content = this.logEntries[rowNum][field];
var index = 0;
while (index < content.length) {
var nextIndex = this.getNextIndex(
this.entryHighlights[rowNum], index, field, content);
// Searches for highlights that have the position in range.
var highlights = this.getHighlight(
this.entryHighlights[rowNum], index, field);
var text = content.substr(index, nextIndex - index);
if (highlights.length > 0) {
// Always picks the highlight with highest priority.
this.addSpan(text, highlights[0].color, parent);
} else {
addNodeWithText(parent, 'span', text);
}
index = nextIndex;
}
},
/**
* A helper function that is used by this.getHightlightedEntry
* It returns the first index where a highlight begins or ends from
* the given index.
* @param {CrosHighlightTag|Array} highlights An array of highlights
* of a log entry.
* @param {int} index The start position.
* @param {string} field The specified field of entry.
* Other fields are ignored.
* @param {string} content The text content of the log entry.
* @return {int} The first index where a highlight begins or ends.
*/
getNextIndex: function(highlights, index, field, content) {
var minGap = Infinity;
var res = -1;
for (var i = 0; i < highlights.length; i++) {
if (highlights[i].field != field || !highlights[i].enabled)
continue;
// Distance between current index and the start index of highlight.
var gap1 = highlights[i].range[0] - index;
// Distance between current index and the end index of highlight.
var gap2 = highlights[i].range[1] - index;
if (gap1 > 0 && gap1 < minGap) {
minGap = gap1;
res = highlights[i].range[0];
}
if (gap2 > 0 && gap2 < minGap) {
minGap = gap2;
res = highlights[i].range[1];
}
}
// Returns |res| if found. Otherwise returns the end position of the text.
return res > 0 ? res : content.length;
},
/**
* A helper function that is used by this.getHightlightedEntry.
* It adds the HTML label to the text.
*/
addSpan: function(text, color, parent) {
var span = addNodeWithText(parent, 'span', text);
span.style.color = color;
span.className = LOG_MARKER_HIGHLIGHT_CLASS;
},
/**
* A helper function that is used by this.getHightlightedEntry.
* It adds the HTML label to the text.
*/
pickColor: function() {
for (var color in COLOR_USAGE_SET) {
if (!COLOR_USAGE_SET[color]) {
COLOR_USAGE_SET[color] = true;
return color;
}
}
return false;
},
/**
* A event handler that enables and disables the corresponding marker.
* @private
*/
onEnableCheckboxChange_: function(checkbox) {
for (var i = 0; i < this.entryHighlights.length; i++) {
for (var j = 0; j < this.entryHighlights[i].length; j++) {
if (this.entryHighlights[i][j].color == checkbox.color) {
this.entryHighlights[i][j].enabled = checkbox.checked;
}
}
}
this.refreshLogTable();
},
/**
* A event handlier that removes the marker from history.
* @private
*/
onRemoveBtnClicked_: function(entry, color) {
entry.parentNode.removeChild(entry);
COLOR_USAGE_SET[color] = false;
for (var i = 0; i < this.entryHighlights.length; i++) {
var highlights = this.entryHighlights[i];
while (true) {
var index = this.findHighlightByColor_(highlights, color);
if (index == -1)
break;
highlights.splice(index, 1);
}
}
this.refreshLogTable();
},
/**
* A helper function that returns the index of first highlight that
* has the target color. Otherwise returns -1.
* @private
*/
findHighlightByColor_: function(highlights, color) {
for (var i = 0; i < highlights.length; i++) {
if (highlights[i].color == color)
return i;
}
return -1;
},
/**
* Refresh the log table in the CrosLogVisualizerView.
*/
refreshLogTable: function() {
this.logVisualizerView.populateTable();
this.logVisualizerView.filterLog();
},
/**
* A pattern can appear multiple times in a string.
* Returns positions of all the appearance.
*/
findPositions: function(pattern, str) {
var res = [];
str = str.toLowerCase();
var match = str.match(pattern);
if (!match)
return res;
for (var i = 0; i < match.length; i++) {
var index = 0;
while (true) {
var start = str.indexOf(match[i].toLowerCase(), index);
if (start == -1)
break;
var end = start + match[i].length;
res.push([start, end]);
index = end + 1;
}
}
return res;
},
/**
* A helper function used in sorting highlights by start position.
* @param {HighlightTag} h1, h2 Two highlight tags in the array.
* @private
*/
compareStartPosition_: function(h1, h2) {
return h1.range[0] - h2.range[0];
},
/**
* A helper function used in sorting highlights by priority.
* @param {HighlightTag} h1, h2 Two highlight tags in the array.
* @private
*/
comparePriority_: function(h1, h2) {
return h2.priority - h1.priority;
},
/**
* A helper function that sorts the highlights array by start position.
* @private
*/
sortHighlightsByStartPosition_: function(highlights) {
highlights.sort(this.compareStartPosition_);
},
/**
* A helper function that sorts the highlights array by priority.
* @private
*/
sortHighlightsByPriority_: function(highlights) {
highlights.sort(this.comparePriority_);
}
};
return CrosLogMarker;
})();