1 // Copyright (c) 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 var SourceFilterParser = (function() { 6 'use strict'; 7 8 /** 9 * Parses |filterText|, extracting a sort method, a list of filters, and a 10 * copy of |filterText| with all sort parameters removed. 11 */ 12 function SourceFilterParser(filterText) { 13 // Final output will be stored here. 14 this.filter = null; 15 this.sort = {}; 16 this.filterTextWithoutSort = ''; 17 var filterList = parseFilter_(filterText); 18 19 // Text filters are stored here as strings and then added as a function at 20 // the end, for performance reasons. 21 var textFilters = []; 22 23 // Filter functions are first created individually, and then merged. 24 var filterFunctions = []; 25 26 for (var i = 0; i < filterList.length; ++i) { 27 var filterElement = filterList[i].parsed; 28 var negated = filterList[i].negated; 29 30 var sort = parseSortDirective_(filterElement, negated); 31 if (sort) { 32 this.sort = sort; 33 continue; 34 } 35 36 this.filterTextWithoutSort += filterList[i].original; 37 38 var filter = parseRestrictDirective_(filterElement, negated); 39 if (!filter) 40 filter = parseStringDirective_(filterElement, negated); 41 if (filter) { 42 if (negated) { 43 filter = (function(func, sourceEntry) { 44 return !func(sourceEntry); 45 }).bind(null, filter); 46 } 47 filterFunctions.push(filter); 48 continue; 49 } 50 textFilters.push({ text: filterElement, negated: negated }); 51 } 52 53 // Create a single filter for all text filters, so they can share a 54 // TabePrinter. 55 filterFunctions.push(textFilter_.bind(null, textFilters)); 56 57 // Create function to go through all the filters. 58 this.filter = function(sourceEntry) { 59 for (var i = 0; i < filterFunctions.length; ++i) { 60 if (!filterFunctions[i](sourceEntry)) 61 return false; 62 } 63 return true; 64 }; 65 } 66 67 /** 68 * Parses a single "sort:" directive, and returns a dictionary containing 69 * the sort function and direction. Returns null on failure, including 70 * the case when no such sort function exists. 71 */ 72 function parseSortDirective_(filterElement, backwards) { 73 var match = /^sort:(.*)$/.exec(filterElement); 74 if (!match) 75 return null; 76 return { method: match[1], backwards: backwards }; 77 } 78 79 /** 80 * Tries to parses |filterElement| as a single "is:" directive, and returns a 81 * new filter function. Returns null on failure. 82 */ 83 function parseRestrictDirective_(filterElement) { 84 var match = /^is:(.*)$/.exec(filterElement); 85 if (!match) 86 return null; 87 if (match[1] == 'active') { 88 return function(sourceEntry) { return !sourceEntry.isInactive(); }; 89 } 90 if (match[1] == 'error') { 91 return function(sourceEntry) { return sourceEntry.isError(); }; 92 } 93 return null; 94 } 95 96 /** 97 * Tries to parse |filterElement| as a single filter of a type that takes 98 * arbitrary strings as input, and returns a new filter function on success. 99 * Returns null on failure. 100 */ 101 function parseStringDirective_(filterElement) { 102 var match = RegExp('^([^:]*):(.*)$').exec(filterElement); 103 if (!match) 104 return null; 105 106 // Split parameters around commas and remove empty elements. 107 var parameters = match[2].split(','); 108 parameters = parameters.filter(function(string) { 109 return string.length > 0; 110 }); 111 112 if (match[1] == 'type') { 113 return function(sourceEntry) { 114 var i; 115 var sourceType = sourceEntry.getSourceTypeString().toLowerCase(); 116 for (i = 0; i < parameters.length; ++i) { 117 if (sourceType.search(parameters[i]) != -1) 118 return true; 119 } 120 return false; 121 }; 122 } 123 124 if (match[1] == 'id') { 125 return function(sourceEntry) { 126 return parameters.indexOf(sourceEntry.getSourceId() + '') != -1; 127 }; 128 } 129 130 return null; 131 } 132 133 /** 134 * Takes in the text of a filter and returns a list of 135 * {parsed, original, negated} values that correspond to substrings of the 136 * filter before and after filtering, and whether or not it started with a 137 * '-'. Extra whitespace other than a single character after each element is 138 * ignored. Parsed strings are all lowercase. 139 */ 140 function parseFilter_(filterText) { 141 // Assemble a list of quoted and unquoted strings in the filter. 142 var filterList = []; 143 var position = 0; 144 while (position < filterText.length) { 145 var inQuote = false; 146 var filterElement = ''; 147 var negated = false; 148 var startPosition = position; 149 while (position < filterText.length) { 150 var nextCharacter = filterText[position]; 151 ++position; 152 if (nextCharacter == '\\' && 153 position < filterText.length) { 154 // If there's a backslash, skip the backslash and add the next 155 // character to the element. 156 filterElement += filterText[position]; 157 ++position; 158 continue; 159 } else if (nextCharacter == '"') { 160 // If there's an unescaped quote character, toggle |inQuote| without 161 // modifying the element. 162 inQuote = !inQuote; 163 } else if (!inQuote && /\s/.test(nextCharacter)) { 164 // If not in a quote and have a whitespace character, that's the 165 // end of the element. 166 break; 167 } else if (nextCharacter == '-' && startPosition == position - 1) { 168 // If this is the first character, and it's a '-', this entry is 169 // negated. 170 negated = true; 171 } else { 172 // Otherwise, add the next character to the element. 173 filterElement += nextCharacter; 174 } 175 } 176 177 if (filterElement.length > 0) { 178 var filter = { 179 parsed: filterElement.toLowerCase(), 180 original: filterText.substring(startPosition, position), 181 negated: negated, 182 }; 183 filterList.push(filter); 184 } 185 } 186 return filterList; 187 } 188 189 /** 190 * Takes in a list of text filters and a SourceEntry. Each filter has 191 * "text" and "negated" fields. Returns true if the SourceEntry matches all 192 * filters in the (possibly empty) list. 193 */ 194 function textFilter_(textFilters, sourceEntry) { 195 var tablePrinter = null; 196 for (var i = 0; i < textFilters.length; ++i) { 197 var text = textFilters[i].text; 198 var negated = textFilters[i].negated; 199 var match = false; 200 // The description is often not contained in one of the log entries. 201 // The source type almost never is, so check for them directly. 202 var description = sourceEntry.getDescription().toLowerCase(); 203 var type = sourceEntry.getSourceTypeString().toLowerCase(); 204 if (description.indexOf(text) != -1 || type.indexOf(text) != -1) { 205 match = true; 206 } else { 207 if (!tablePrinter) 208 tablePrinter = sourceEntry.createTablePrinter(); 209 match = tablePrinter.search(text); 210 } 211 if (negated) 212 match = !match; 213 if (!match) 214 return false; 215 } 216 return true; 217 } 218 219 return SourceFilterParser; 220 })(); 221