// Copyright (c) 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. | |
var SourceFilterParser = (function() { | |
'use strict'; | |
/** | |
* Parses |filterText|, extracting a sort method, a list of filters, and a | |
* copy of |filterText| with all sort parameters removed. | |
*/ | |
function SourceFilterParser(filterText) { | |
// Final output will be stored here. | |
this.filter = null; | |
this.sort = {}; | |
this.filterTextWithoutSort = ''; | |
var filterList = parseFilter_(filterText); | |
// Text filters are stored here as strings and then added as a function at | |
// the end, for performance reasons. | |
var textFilters = []; | |
// Filter functions are first created individually, and then merged. | |
var filterFunctions = []; | |
for (var i = 0; i < filterList.length; ++i) { | |
var filterElement = filterList[i].parsed; | |
var negated = filterList[i].negated; | |
var sort = parseSortDirective_(filterElement, negated); | |
if (sort) { | |
this.sort = sort; | |
continue; | |
} | |
this.textWithoutSort += filterList[i].original; | |
var filter = parseRestrictDirective_(filterElement, negated); | |
if (!filter) | |
filter = parseStringDirective_(filterElement, negated); | |
if (filter) { | |
if (negated) { | |
filter = (function(func, sourceEntry) { | |
return !func(sourceEntry); | |
}).bind(null, filter); | |
} | |
filterFunctions.push(filter); | |
continue; | |
} | |
textFilters.push({ text: filterElement, negated: negated }); | |
} | |
// Create a single filter for all text filters, so they can share a | |
// TabePrinter. | |
filterFunctions.push(textFilter_.bind(null, textFilters)); | |
// Create function to go through all the filters. | |
this.filter = function(sourceEntry) { | |
for (var i = 0; i < filterFunctions.length; ++i) { | |
if (!filterFunctions[i](sourceEntry)) | |
return false; | |
} | |
return true; | |
}; | |
} | |
/** | |
* Parses a single "sort:" directive, and returns a dictionary containing | |
* the sort function and direction. Returns null on failure, including | |
* the case when no such sort function exists. | |
*/ | |
function parseSortDirective_(filterElement, backwards) { | |
var match = /^sort:(.*)$/.exec(filterElement); | |
if (!match) | |
return null; | |
return { method: match[1], backwards: backwards }; | |
} | |
/** | |
* Tries to parses |filterElement| as a single "is:" directive, and returns a | |
* new filter function. Returns null on failure. | |
*/ | |
function parseRestrictDirective_(filterElement) { | |
var match = /^is:(.*)$/.exec(filterElement); | |
if (!match) | |
return null; | |
if (match[1] == 'active') { | |
return function(sourceEntry) { return !sourceEntry.isInactive(); }; | |
} | |
if (match[1] == 'error') { | |
return function(sourceEntry) { return sourceEntry.isError(); }; | |
} | |
return null; | |
} | |
/** | |
* Tries to parse |filterElement| as a single filter of a type that takes | |
* arbitrary strings as input, and returns a new filter function on success. | |
* Returns null on failure. | |
*/ | |
function parseStringDirective_(filterElement) { | |
var match = RegExp('^([^:]*):(.*)$').exec(filterElement); | |
if (!match) | |
return null; | |
// Split parameters around commas and remove empty elements. | |
var parameters = match[2].split(','); | |
parameters = parameters.filter(function(string) { | |
return string.length > 0; | |
}); | |
if (match[1] == 'type') { | |
return function(sourceEntry) { | |
var i; | |
var sourceType = sourceEntry.getSourceTypeString().toLowerCase(); | |
for (i = 0; i < parameters.length; ++i) { | |
if (sourceType.search(parameters[i]) != -1) | |
return true; | |
} | |
return false; | |
}; | |
} | |
if (match[1] == 'id') { | |
return function(sourceEntry) { | |
return parameters.indexOf(sourceEntry.getSourceId() + '') != -1; | |
}; | |
} | |
return null; | |
} | |
/** | |
* Takes in the text of a filter and returns a list of | |
* {parsed, original, negated} values that correspond to substrings of the | |
* filter before and after filtering, and whether or not it started with a | |
* '-'. Extra whitespace other than a single character after each element is | |
* ignored. Parsed strings are all lowercase. | |
*/ | |
function parseFilter_(filterText) { | |
// Assemble a list of quoted and unquoted strings in the filter. | |
var filterList = []; | |
var position = 0; | |
while (position < filterText.length) { | |
var inQuote = false; | |
var filterElement = ''; | |
var negated = false; | |
var startPosition = position; | |
while (position < filterText.length) { | |
var nextCharacter = filterText[position]; | |
++position; | |
if (nextCharacter == '\\' && | |
position < filterText.length) { | |
// If there's a backslash, skip the backslash and add the next | |
// character to the element. | |
filterElement += filterText[position]; | |
++position; | |
continue; | |
} else if (nextCharacter == '"') { | |
// If there's an unescaped quote character, toggle |inQuote| without | |
// modifying the element. | |
inQuote = !inQuote; | |
} else if (!inQuote && /\s/.test(nextCharacter)) { | |
// If not in a quote and have a whitespace character, that's the | |
// end of the element. | |
break; | |
} else if (nextCharacter == '-' && startPosition == position - 1) { | |
// If this is the first character, and it's a '-', this entry is | |
// negated. | |
negated = true; | |
} else { | |
// Otherwise, add the next character to the element. | |
filterElement += nextCharacter; | |
} | |
} | |
if (filterElement.length > 0) { | |
var filter = { | |
parsed: filterElement.toLowerCase(), | |
original: filterText.substring(startPosition, position), | |
negated: negated, | |
}; | |
filterList.push(filter); | |
} | |
} | |
return filterList; | |
} | |
/** | |
* Takes in a list of text filters and a SourceEntry. Each filter has | |
* "text" and "negated" fields. Returns true if the SourceEntry matches all | |
* filters in the (possibly empty) list. | |
*/ | |
function textFilter_(textFilters, sourceEntry) { | |
var tablePrinter = null; | |
for (var i = 0; i < textFilters.length; ++i) { | |
var text = textFilters[i].text; | |
var negated = textFilters[i].negated; | |
var match = false; | |
// The description is often not contained in one of the log entries. | |
// The source type almost never is, so check for them directly. | |
var description = sourceEntry.getDescription().toLowerCase(); | |
var type = sourceEntry.getSourceTypeString().toLowerCase(); | |
if (description.indexOf(text) != -1 || type.indexOf(text) != -1) { | |
match = true; | |
} else { | |
if (!tablePrinter) { | |
tablePrinter = createLogEntryTablePrinter( | |
sourceEntry.getLogEntries(), | |
SourceTracker.getInstance().getPrivacyStripping()); | |
} | |
match = tablePrinter.search(text); | |
} | |
if (negated) | |
match = !match; | |
if (!match) | |
return false; | |
} | |
return true; | |
} | |
return SourceFilterParser; | |
})(); |