##// END OF EJS Templates
search: add syntax highlighting, line numbers and line context...
dan -
r48:5de64805 default
parent child Browse files
Show More
@@ -0,0 +1,490 b''
1 /*!***************************************************
2 * mark.js v6.1.0
3 * https://github.com/julmot/mark.js
4 * Copyright (c) 2014–2016, Julian Motz
5 * Released under the MIT license https://git.io/vwTVl
6 *****************************************************/
7
8 "use strict";
9
10 var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
11
12 var _createClass = function () { function defineProperties(target, props) { for (var i = 0; i < props.length; i++) { var descriptor = props[i]; descriptor.enumerable = descriptor.enumerable || false; descriptor.configurable = true; if ("value" in descriptor) descriptor.writable = true; Object.defineProperty(target, descriptor.key, descriptor); } } return function (Constructor, protoProps, staticProps) { if (protoProps) defineProperties(Constructor.prototype, protoProps); if (staticProps) defineProperties(Constructor, staticProps); return Constructor; }; }();
13
14 var _typeof = typeof Symbol === "function" && typeof Symbol.iterator === "symbol" ? function (obj) { return typeof obj; } : function (obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol ? "symbol" : typeof obj; };
15
16 function _classCallCheck(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new TypeError("Cannot call a class as a function"); } }
17
18 (function (factory, window, document) {
19 if (typeof define === "function" && define.amd) {
20 define(["jquery"], function (jQuery) {
21 return factory(window, document, jQuery);
22 });
23 } else if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === "object") {
24 factory(window, document, require("jquery"));
25 } else {
26 factory(window, document, jQuery);
27 }
28 })(function (window, document, $) {
29 var Mark = function () {
30 function Mark(ctx) {
31 _classCallCheck(this, Mark);
32
33 this.ctx = ctx;
34 }
35
36 _createClass(Mark, [{
37 key: "log",
38 value: function log(msg) {
39 var level = arguments.length <= 1 || arguments[1] === undefined ? "debug" : arguments[1];
40
41 var log = this.opt.log;
42 if (!this.opt.debug) {
43 return;
44 }
45 if ((typeof log === "undefined" ? "undefined" : _typeof(log)) === "object" && typeof log[level] === "function") {
46 log[level]("mark.js: " + msg);
47 }
48 }
49 }, {
50 key: "escapeStr",
51 value: function escapeStr(str) {
52 return str.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
53 }
54 }, {
55 key: "createRegExp",
56 value: function createRegExp(str) {
57 str = this.escapeStr(str);
58 if (Object.keys(this.opt.synonyms).length) {
59 str = this.createSynonymsRegExp(str);
60 }
61 if (this.opt.diacritics) {
62 str = this.createDiacriticsRegExp(str);
63 }
64 str = this.createAccuracyRegExp(str);
65 return str;
66 }
67 }, {
68 key: "createSynonymsRegExp",
69 value: function createSynonymsRegExp(str) {
70 var syn = this.opt.synonyms;
71 for (var index in syn) {
72 if (syn.hasOwnProperty(index)) {
73 var value = syn[index],
74 k1 = this.escapeStr(index),
75 k2 = this.escapeStr(value);
76 str = str.replace(new RegExp("(" + k1 + "|" + k2 + ")", "gmi"), "(" + k1 + "|" + k2 + ")");
77 }
78 }
79 return str;
80 }
81 }, {
82 key: "createDiacriticsRegExp",
83 value: function createDiacriticsRegExp(str) {
84 var dct = ["aÀÁÂÃÄÅàáâãäåĀāąĄ", "cÇçćĆčČ", "dđĐďĎ", "eÈÉÊËèéêëěĚĒēęĘ", "iÌÍÎÏìíîïĪī", "lłŁ", "nÑñňŇńŃ", "oÒÓÔÕÕÖØòóôõöøŌō", "rřŘ", "sŠšśŚ", "tťŤ", "uÙÚÛÜùúûüůŮŪū", "yŸÿýÝ", "zŽžżŻźŹ"];
85 var handled = [];
86 str.split("").forEach(function (ch) {
87 dct.every(function (dct) {
88 if (dct.indexOf(ch) !== -1) {
89 if (handled.indexOf(dct) > -1) {
90 return false;
91 }
92
93 str = str.replace(new RegExp("[" + dct + "]", "gmi"), "[" + dct + "]");
94 handled.push(dct);
95 }
96 return true;
97 });
98 });
99 return str;
100 }
101 }, {
102 key: "createAccuracyRegExp",
103 value: function createAccuracyRegExp(str) {
104 switch (this.opt.accuracy) {
105 case "partially":
106 return "()(" + str + ")";
107 case "complementary":
108 return "()(\\S*" + str + "\\S*)";
109 case "exactly":
110 return "(^|\\s)(" + str + ")(?=\\s|$)";
111 }
112 }
113 }, {
114 key: "getSeparatedKeywords",
115 value: function getSeparatedKeywords(sv) {
116 var _this = this;
117
118 var stack = [];
119 sv.forEach(function (kw) {
120 if (!_this.opt.separateWordSearch) {
121 if (kw.trim()) {
122 stack.push(kw);
123 }
124 } else {
125 kw.split(" ").forEach(function (kwSplitted) {
126 if (kwSplitted.trim()) {
127 stack.push(kwSplitted);
128 }
129 });
130 }
131 });
132 return {
133 "keywords": stack,
134 "length": stack.length
135 };
136 }
137 }, {
138 key: "getElements",
139 value: function getElements() {
140 var ctx = void 0,
141 stack = [];
142 if (typeof this.ctx === "undefined") {
143 ctx = [];
144 } else if (this.ctx instanceof HTMLElement) {
145 ctx = [this.ctx];
146 } else if (Array.isArray(this.ctx)) {
147 ctx = this.ctx;
148 } else {
149 ctx = Array.prototype.slice.call(this.ctx);
150 }
151 ctx.forEach(function (ctx) {
152 stack.push(ctx);
153 var childs = ctx.querySelectorAll("*");
154 if (childs.length) {
155 stack = stack.concat(Array.prototype.slice.call(childs));
156 }
157 });
158 if (!ctx.length) {
159 this.log("Empty context", "warn");
160 }
161 return {
162 "elements": stack,
163 "length": stack.length
164 };
165 }
166 }, {
167 key: "matches",
168 value: function matches(el, selector) {
169 return (el.matches || el.matchesSelector || el.msMatchesSelector || el.mozMatchesSelector || el.webkitMatchesSelector || el.oMatchesSelector).call(el, selector);
170 }
171 }, {
172 key: "matchesFilter",
173 value: function matchesFilter(el, exclM) {
174 var _this2 = this;
175
176 var remain = true;
177 var fltr = this.opt.filter.concat(["script", "style", "title"]);
178 if (!this.opt.iframes) {
179 fltr = fltr.concat(["iframe"]);
180 }
181 if (exclM) {
182 fltr = fltr.concat(["*[data-markjs='true']"]);
183 }
184 fltr.every(function (filter) {
185 if (_this2.matches(el, filter)) {
186 return remain = false;
187 }
188 return true;
189 });
190 return !remain;
191 }
192 }, {
193 key: "onIframeReady",
194 value: function onIframeReady(ifr, successFn, errorFn) {
195 try {
196 (function () {
197 var ifrWin = ifr.contentWindow,
198 bl = "about:blank",
199 compl = "complete";
200 var callCallback = function callCallback() {
201 try {
202 if (ifrWin.document === null) {
203 throw new Error("iframe inaccessible");
204 }
205 successFn(ifrWin.document);
206 } catch (e) {
207 errorFn();
208 }
209 };
210 var isBlank = function isBlank() {
211 var src = ifr.getAttribute("src").trim(),
212 href = ifrWin.location.href;
213 return href === bl && src !== bl && src;
214 };
215 var observeOnload = function observeOnload() {
216 var listener = function listener() {
217 try {
218 if (!isBlank()) {
219 ifr.removeEventListener("load", listener);
220 callCallback();
221 }
222 } catch (e) {
223 errorFn();
224 }
225 };
226 ifr.addEventListener("load", listener);
227 };
228 if (ifrWin.document.readyState === compl) {
229 if (isBlank()) {
230 observeOnload();
231 } else {
232 callCallback();
233 }
234 } else {
235 observeOnload();
236 }
237 })();
238 } catch (e) {
239 errorFn();
240 }
241 }
242 }, {
243 key: "forEachElementInIframe",
244 value: function forEachElementInIframe(ifr, cb) {
245 var _this3 = this;
246
247 var end = arguments.length <= 2 || arguments[2] === undefined ? function () {} : arguments[2];
248
249 var open = 0;
250 var checkEnd = function checkEnd() {
251 if (--open < 1) {
252 end();
253 }
254 };
255 this.onIframeReady(ifr, function (con) {
256 var stack = Array.prototype.slice.call(con.querySelectorAll("*"));
257 if ((open = stack.length) === 0) {
258 checkEnd();
259 }
260 stack.forEach(function (el) {
261 if (el.tagName.toLowerCase() === "iframe") {
262 (function () {
263 var j = 0;
264 _this3.forEachElementInIframe(el, function (iel, len) {
265 cb(iel, len);
266 if (len - 1 === j) {
267 checkEnd();
268 }
269 j++;
270 }, checkEnd);
271 })();
272 } else {
273 cb(el, stack.length);
274 checkEnd();
275 }
276 });
277 }, function () {
278 var src = ifr.getAttribute("src");
279 _this3.log("iframe '" + src + "' could not be accessed", "warn");
280 checkEnd();
281 });
282 }
283 }, {
284 key: "forEachElement",
285 value: function forEachElement(cb) {
286 var _this4 = this;
287
288 var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1];
289 var exclM = arguments.length <= 2 || arguments[2] === undefined ? true : arguments[2];
290
291 var _getElements = this.getElements();
292
293 var stack = _getElements.elements;
294 var open = _getElements.length;
295
296 var checkEnd = function checkEnd() {
297 if (--open === 0) {
298 end();
299 }
300 };
301 checkEnd(++open);
302 stack.forEach(function (el) {
303 if (!_this4.matchesFilter(el, exclM)) {
304 if (el.tagName.toLowerCase() === "iframe") {
305 _this4.forEachElementInIframe(el, function (iel) {
306 if (!_this4.matchesFilter(iel, exclM)) {
307 cb(iel);
308 }
309 }, checkEnd);
310 return;
311 } else {
312 cb(el);
313 }
314 }
315 checkEnd();
316 });
317 }
318 }, {
319 key: "forEachNode",
320 value: function forEachNode(cb) {
321 var end = arguments.length <= 1 || arguments[1] === undefined ? function () {} : arguments[1];
322
323 this.forEachElement(function (n) {
324 for (n = n.firstChild; n; n = n.nextSibling) {
325 if (n.nodeType === 3 && n.textContent.trim()) {
326 cb(n);
327 }
328 }
329 }, end);
330 }
331 }, {
332 key: "wrapMatches",
333 value: function wrapMatches(node, regex, custom, cb) {
334 var hEl = !this.opt.element ? "mark" : this.opt.element,
335 index = custom ? 0 : 2;
336 var match = void 0;
337 while ((match = regex.exec(node.textContent)) !== null) {
338 var pos = match.index;
339 if (!custom) {
340 pos += match[index - 1].length;
341 }
342 var startNode = node.splitText(pos);
343
344 node = startNode.splitText(match[index].length);
345 if (startNode.parentNode !== null) {
346 var repl = document.createElement(hEl);
347 repl.setAttribute("data-markjs", "true");
348 if (this.opt.className) {
349 repl.setAttribute("class", this.opt.className);
350 }
351 repl.textContent = match[index];
352 startNode.parentNode.replaceChild(repl, startNode);
353 cb(repl);
354 }
355 regex.lastIndex = 0;
356 }
357 }
358 }, {
359 key: "unwrapMatches",
360 value: function unwrapMatches(node) {
361 var parent = node.parentNode;
362 var docFrag = document.createDocumentFragment();
363 while (node.firstChild) {
364 docFrag.appendChild(node.removeChild(node.firstChild));
365 }
366 parent.replaceChild(docFrag, node);
367 parent.normalize();
368 }
369 }, {
370 key: "markRegExp",
371 value: function markRegExp(regexp, opt) {
372 var _this5 = this;
373
374 this.opt = opt;
375 this.log("Searching with expression \"" + regexp + "\"");
376 var found = false;
377 var eachCb = function eachCb(element) {
378 found = true;
379 _this5.opt.each(element);
380 };
381 this.forEachNode(function (node) {
382 _this5.wrapMatches(node, regexp, true, eachCb);
383 }, function () {
384 if (!found) {
385 _this5.opt.noMatch(regexp);
386 }
387 _this5.opt.complete();
388 _this5.opt.done();
389 });
390 }
391 }, {
392 key: "mark",
393 value: function mark(sv, opt) {
394 var _this6 = this;
395
396 this.opt = opt;
397 sv = typeof sv === "string" ? [sv] : sv;
398
399 var _getSeparatedKeywords = this.getSeparatedKeywords(sv);
400
401 var kwArr = _getSeparatedKeywords.keywords;
402 var kwArrLen = _getSeparatedKeywords.length;
403
404 if (kwArrLen === 0) {
405 this.opt.complete();
406 this.opt.done();
407 }
408 kwArr.forEach(function (kw) {
409 var regex = new RegExp(_this6.createRegExp(kw), "gmi"),
410 found = false;
411 var eachCb = function eachCb(element) {
412 found = true;
413 _this6.opt.each(element);
414 };
415 _this6.log("Searching with expression \"" + regex + "\"");
416 _this6.forEachNode(function (node) {
417 _this6.wrapMatches(node, regex, false, eachCb);
418 }, function () {
419 if (!found) {
420 _this6.opt.noMatch(kw);
421 }
422 if (kwArr[kwArrLen - 1] === kw) {
423 _this6.opt.complete();
424 _this6.opt.done();
425 }
426 });
427 });
428 }
429 }, {
430 key: "unmark",
431 value: function unmark(opt) {
432 var _this7 = this;
433
434 this.opt = opt;
435 var sel = this.opt.element ? this.opt.element : "*";
436 sel += "[data-markjs]";
437 if (this.opt.className) {
438 sel += "." + this.opt.className;
439 }
440 this.log("Removal selector \"" + sel + "\"");
441 this.forEachElement(function (el) {
442 if (_this7.matches(el, sel)) {
443 _this7.unwrapMatches(el);
444 }
445 }, function () {
446 _this7.opt.complete();
447 _this7.opt.done();
448 }, false);
449 }
450 }, {
451 key: "opt",
452 set: function set(val) {
453 this._opt = _extends({}, {
454 "element": "",
455 "className": "",
456 "filter": [],
457 "iframes": false,
458 "separateWordSearch": true,
459 "diacritics": true,
460 "synonyms": {},
461 "accuracy": "partially",
462 "each": function each() {},
463 "noMatch": function noMatch() {},
464 "done": function done() {},
465 "complete": function complete() {},
466 "debug": false,
467 "log": window.console
468 }, val);
469 },
470 get: function get() {
471 return this._opt;
472 }
473 }]);
474
475 return Mark;
476 }();
477
478 $.fn.mark = function (sv, opt) {
479 new Mark(this).mark(sv, opt);
480 return this;
481 };
482 $.fn.markRegExp = function (regexp, opt) {
483 new Mark(this).markRegExp(regexp, opt);
484 return this;
485 };
486 $.fn.unmark = function (opt) {
487 new Mark(this).unmark(opt);
488 return this;
489 };
490 }, window, document);
@@ -18,6 +18,7 b' module.exports = function(grunt) {'
18 18 '<%= dirs.js.src %>/bootstrap.js',
19 19 '<%= dirs.js.src %>/mousetrap.js',
20 20 '<%= dirs.js.src %>/moment.js',
21 '<%= dirs.js.src %>/moment.js',
21 22 '<%= dirs.js.src %>/appenlight-client-0.4.1.min.js',
22 23
23 24 // Plugins
@@ -27,12 +28,13 b' module.exports = function(grunt) {'
27 28 '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js',
28 29 '<%= dirs.js.src %>/plugins/jquery.autocomplete.js',
29 30 '<%= dirs.js.src %>/plugins/jquery.debounce.js',
31 '<%= dirs.js.src %>/plugins/jquery.mark.js',
30 32 '<%= dirs.js.src %>/plugins/jquery.timeago.js',
31 33 '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js',
32 34
33 35 // Select2
34 36 '<%= dirs.js.src %>/select2/select2.js',
35
37
36 38 // Code-mirror
37 39 '<%= dirs.js.src %>/codemirror/codemirror.js',
38 40 '<%= dirs.js.src %>/codemirror/codemirror_loadmode.js',
@@ -79,7 +79,8 b' class SearchController(BaseRepoControlle'
79 79
80 80 try:
81 81 search_result = searcher.search(
82 search_query, search_type, c.perm_user, repo_name)
82 search_query, search_type, c.perm_user, repo_name,
83 requested_page, page_limit)
83 84
84 85 formatted_results = Page(
85 86 search_result['results'], page=requested_page,
@@ -36,11 +36,14 b' import urlparse'
36 36 import time
37 37 import string
38 38 import hashlib
39 import pygments
39 40
40 41 from datetime import datetime
41 42 from functools import partial
42 43 from pygments.formatters.html import HtmlFormatter
43 44 from pygments import highlight as code_highlight
45 from pygments.lexers import (
46 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
44 47 from pylons import url
45 48 from pylons.i18n.translation import _, ungettext
46 49 from pyramid.threadlocal import get_current_request
@@ -307,6 +310,176 b' class CodeHtmlFormatter(HtmlFormatter):'
307 310 yield 0, '</td></tr></table>'
308 311
309 312
313 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
314 def __init__(self, **kw):
315 # only show these line numbers if set
316 self.only_lines = kw.pop('only_line_numbers', [])
317 self.query_terms = kw.pop('query_terms', [])
318 self.max_lines = kw.pop('max_lines', 5)
319 self.line_context = kw.pop('line_context', 3)
320 self.url = kw.pop('url', None)
321
322 super(CodeHtmlFormatter, self).__init__(**kw)
323
324 def _wrap_code(self, source):
325 for cnt, it in enumerate(source):
326 i, t = it
327 t = '<pre>%s</pre>' % t
328 yield i, t
329
330 def _wrap_tablelinenos(self, inner):
331 yield 0, '<table class="code-highlight %stable">' % self.cssclass
332
333 last_shown_line_number = 0
334 current_line_number = 1
335
336 for t, line in inner:
337 if not t:
338 yield t, line
339 continue
340
341 if current_line_number in self.only_lines:
342 if last_shown_line_number + 1 != current_line_number:
343 yield 0, '<tr>'
344 yield 0, '<td class="line">...</td>'
345 yield 0, '<td id="hlcode" class="code"></td>'
346 yield 0, '</tr>'
347
348 yield 0, '<tr>'
349 if self.url:
350 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
351 self.url, current_line_number, current_line_number)
352 else:
353 yield 0, '<td class="line"><a href="">%i</a></td>' % (
354 current_line_number)
355 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
356 yield 0, '</tr>'
357
358 last_shown_line_number = current_line_number
359
360 current_line_number += 1
361
362
363 yield 0, '</table>'
364
365
366 def extract_phrases(text_query):
367 """
368 Extracts phrases from search term string making sure phrases
369 contained in double quotes are kept together - and discarding empty values
370 or fully whitespace values eg.
371
372 'some text "a phrase" more' => ['some', 'text', 'a phrase', 'more']
373
374 """
375
376 in_phrase = False
377 buf = ''
378 phrases = []
379 for char in text_query:
380 if in_phrase:
381 if char == '"': # end phrase
382 phrases.append(buf)
383 buf = ''
384 in_phrase = False
385 continue
386 else:
387 buf += char
388 continue
389 else:
390 if char == '"': # start phrase
391 in_phrase = True
392 phrases.append(buf)
393 buf = ''
394 continue
395 elif char == ' ':
396 phrases.append(buf)
397 buf = ''
398 continue
399 else:
400 buf += char
401
402 phrases.append(buf)
403 phrases = [phrase.strip() for phrase in phrases if phrase.strip()]
404 return phrases
405
406
407 def get_matching_offsets(text, phrases):
408 """
409 Returns a list of string offsets in `text` that the list of `terms` match
410
411 >>> get_matching_offsets('some text here', ['some', 'here'])
412 [(0, 4), (10, 14)]
413
414 """
415 offsets = []
416 for phrase in phrases:
417 for match in re.finditer(phrase, text):
418 offsets.append((match.start(), match.end()))
419
420 return offsets
421
422
423 def normalize_text_for_matching(x):
424 """
425 Replaces all non alnum characters to spaces and lower cases the string,
426 useful for comparing two text strings without punctuation
427 """
428 return re.sub(r'[^\w]', ' ', x.lower())
429
430
431 def get_matching_line_offsets(lines, terms):
432 """ Return a set of `lines` indices (starting from 1) matching a
433 text search query, along with `context` lines above/below matching lines
434
435 :param lines: list of strings representing lines
436 :param terms: search term string to match in lines eg. 'some text'
437 :param context: number of lines above/below a matching line to add to result
438 :param max_lines: cut off for lines of interest
439 eg.
440
441 >>> get_matching_line_offsets('''
442 words words words
443 words words words
444 some text some
445 words words words
446 words words words
447 text here what
448 ''', 'text', context=1)
449 {3: [(5, 9)], 6: [(0, 4)]]
450 """
451 matching_lines = {}
452 phrases = [normalize_text_for_matching(phrase)
453 for phrase in extract_phrases(terms)]
454
455 for line_index, line in enumerate(lines, start=1):
456 match_offsets = get_matching_offsets(
457 normalize_text_for_matching(line), phrases)
458 if match_offsets:
459 matching_lines[line_index] = match_offsets
460
461 return matching_lines
462
463 def get_lexer_safe(mimetype=None, filepath=None):
464 """
465 Tries to return a relevant pygments lexer using mimetype/filepath name,
466 defaulting to plain text if none could be found
467 """
468 lexer = None
469 try:
470 if mimetype:
471 lexer = get_lexer_for_mimetype(mimetype)
472 if not lexer:
473 lexer = get_lexer_for_filename(path)
474 except pygments.util.ClassNotFound:
475 pass
476
477 if not lexer:
478 lexer = get_lexer_by_name('text')
479
480 return lexer
481
482
310 483 def pygmentize(filenode, **kwargs):
311 484 """
312 485 pygmentize function using pygments
@@ -90,7 +90,8 b' class Search(BaseSearch):'
90 90 if self.searcher:
91 91 self.searcher.close()
92 92
93 def search(self, query, document_type, search_user, repo_name=None):
93 def search(self, query, document_type, search_user, repo_name=None,
94 requested_page=1, page_limit=10):
94 95 log.debug(u'QUERY: %s on %s', query, document_type)
95 96 result = {
96 97 'results': [],
@@ -514,6 +514,26 b' div.search-code-body {'
514 514 .match { background-color: #faffa6;}
515 515 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
516 516 }
517 .code-highlighttable {
518 border-collapse: collapse;
519
520 tr:hover {
521 background: #fafafa;
522 }
523 td.code {
524 padding-left: 10px;
525 }
526 td.line {
527 border-right: 1px solid #ccc !important;
528 padding-right: 10px;
529 text-align: right;
530 font-family: "Lucida Console",Monaco,monospace;
531 span {
532 white-space: pre-wrap;
533 color: #666666;
534 }
535 }
536 }
517 537 }
518 538
519 539 div.annotatediv { margin-left: 2px; margin-right: 4px; }
@@ -33,7 +33,7 b''
33 33 </div>
34 34 </td>
35 35 <td data-commit-id="${h.md5_safe(entry['repository'])+entry['commit_id']}" id="c-${h.md5_safe(entry['repository'])+entry['commit_id']}" class="message td-description open">
36 %if entry['message_hl']:
36 %if entry.get('message_hl'):
37 37 ${h.literal(entry['message_hl'])}
38 38 %else:
39 39 ${h.urlify_commit_message(entry['message'], entry['repository'])}
@@ -1,3 +1,40 b''
1 <%def name="highlight_text_file(terms, text, url, line_context=3,
2 max_lines=10,
3 mimetype=None, filepath=None)">
4 <%
5 lines = text.split('\n')
6 lines_of_interest = set()
7 matching_lines = h.get_matching_line_offsets(lines, terms)
8 shown_matching_lines = 0
9
10 for line_number in matching_lines:
11 if len(lines_of_interest) < max_lines:
12 lines_of_interest |= set(range(
13 max(line_number - line_context, 0),
14 min(line_number + line_context, len(lines))))
15 shown_matching_lines += 1
16
17 %>
18 ${h.code_highlight(
19 text,
20 h.get_lexer_safe(
21 mimetype=mimetype,
22 filepath=filepath,
23 ),
24 h.SearchContentCodeHtmlFormatter(
25 linenos=True,
26 cssclass="code-highlight",
27 url=url,
28 query_terms=terms,
29 only_line_numbers=lines_of_interest
30 ))|n}
31 %if len(matching_lines) > shown_matching_lines:
32 <a href="${url}">
33 ${len(matching_lines) - shown_matching_lines} ${_('more matches in this file')}
34 </p>
35 %endif
36 </%def>
37
1 38 <div class="search-results">
2 39 %for entry in c.formatted_results:
3 40 ## search results are additionally filtered, and this check is just a safe gate
@@ -29,7 +66,7 b''
29 66 <div class="buttons">
30 67 <a id="file_history_overview_full" href="${h.url('changelog_file_home',repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
31 68 ${_('Show Full History')}
32 </a> |
69 </a> |
33 70 ${h.link_to(_('Annotation'), h.url('files_annotate_home', repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
34 71 | ${h.link_to(_('Raw'), h.url('files_raw_home', repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path','')))}
35 72 | <a href="${h.url('files_rawfile_home',repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}">
@@ -38,8 +75,10 b''
38 75 </div>
39 76 </div>
40 77 <div class="code-body search-code-body">
41 <pre>${h.literal(entry['content_short_hl'])}</pre>
42 </div>
78 ${highlight_text_file(c.cur_query, entry['content'],
79 url=h.url('files_home',repo_name=entry['repository'],revision=entry.get('commit_id', 'tip'),f_path=entry['f_path']),
80 mimetype=entry.get('mimetype'), filepath=entry.get('path'))}
81 </div>
43 82 </div>
44 83 % endif
45 84 %endfor
@@ -49,3 +88,14 b''
49 88 ${c.formatted_results.pager('$link_previous ~2~ $link_next')}
50 89 </div>
51 90 %endif
91
92 %if c.cur_query:
93 <script type="text/javascript">
94 $(function(){
95 $(".code").mark(
96 '${' '.join(h.normalize_text_for_matching(c.cur_query).split())}',
97 {"className": 'match',
98 });
99 })
100 </script>
101 %endif No newline at end of file
@@ -155,3 +155,42 b' def test_get_visual_attr(pylonsapp):'
155 155 def test_chop_at(test_text, inclusive, expected_text):
156 156 assert helpers.chop_at_smart(
157 157 test_text, '\n', inclusive, '...') == expected_text
158
159
160 @pytest.mark.parametrize('test_text, expected_output', [
161 ('some text', ['some', 'text']),
162 ('some text', ['some', 'text']),
163 ('some text "with a phrase"', ['some', 'text', 'with a phrase']),
164 ('"a phrase" "another phrase"', ['a phrase', 'another phrase']),
165 ('"justphrase"', ['justphrase']),
166 ('""', []),
167 ('', []),
168 (' ', []),
169 ('" "', []),
170 ])
171 def test_extract_phrases(test_text, expected_output):
172 assert helpers.extract_phrases(test_text) == expected_output
173
174
175 @pytest.mark.parametrize('test_text, text_phrases, expected_output', [
176 ('some text here', ['some', 'here'], [(0, 4), (10, 14)]),
177 ('here here there', ['here'], [(0, 4), (5, 9), (11, 15)]),
178 ('irrelevant', ['not found'], []),
179 ('irrelevant', ['not found'], []),
180 ])
181 def test_get_matching_offsets(test_text, text_phrases, expected_output):
182 assert helpers.get_matching_offsets(
183 test_text, text_phrases) == expected_output
184
185 def test_normalize_text_for_matching():
186 assert helpers.normalize_text_for_matching(
187 'OJjfe)*#$*@)$JF*)3r2f80h') == 'ojjfe jf 3r2f80h'
188
189 def test_get_matching_line_offsets():
190 assert helpers.get_matching_line_offsets([
191 'words words words',
192 'words words words',
193 'some text some',
194 'words words words',
195 'words words words',
196 'text here what'], 'text') == {3: [(5, 9)], 6: [(0, 4)]} No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now