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 | '<%= dirs.js.src %>/bootstrap.js', |
|
18 | '<%= dirs.js.src %>/bootstrap.js', | |
19 | '<%= dirs.js.src %>/mousetrap.js', |
|
19 | '<%= dirs.js.src %>/mousetrap.js', | |
20 | '<%= dirs.js.src %>/moment.js', |
|
20 | '<%= dirs.js.src %>/moment.js', | |
|
21 | '<%= dirs.js.src %>/moment.js', | |||
21 | '<%= dirs.js.src %>/appenlight-client-0.4.1.min.js', |
|
22 | '<%= dirs.js.src %>/appenlight-client-0.4.1.min.js', | |
22 |
|
23 | |||
23 | // Plugins |
|
24 | // Plugins | |
@@ -27,12 +28,13 b' module.exports = function(grunt) {' | |||||
27 | '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js', |
|
28 | '<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js', | |
28 | '<%= dirs.js.src %>/plugins/jquery.autocomplete.js', |
|
29 | '<%= dirs.js.src %>/plugins/jquery.autocomplete.js', | |
29 | '<%= dirs.js.src %>/plugins/jquery.debounce.js', |
|
30 | '<%= dirs.js.src %>/plugins/jquery.debounce.js', | |
|
31 | '<%= dirs.js.src %>/plugins/jquery.mark.js', | |||
30 | '<%= dirs.js.src %>/plugins/jquery.timeago.js', |
|
32 | '<%= dirs.js.src %>/plugins/jquery.timeago.js', | |
31 | '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', |
|
33 | '<%= dirs.js.src %>/plugins/jquery.timeago-extension.js', | |
32 |
|
34 | |||
33 | // Select2 |
|
35 | // Select2 | |
34 | '<%= dirs.js.src %>/select2/select2.js', |
|
36 | '<%= dirs.js.src %>/select2/select2.js', | |
35 |
|
37 | |||
36 | // Code-mirror |
|
38 | // Code-mirror | |
37 | '<%= dirs.js.src %>/codemirror/codemirror.js', |
|
39 | '<%= dirs.js.src %>/codemirror/codemirror.js', | |
38 | '<%= dirs.js.src %>/codemirror/codemirror_loadmode.js', |
|
40 | '<%= dirs.js.src %>/codemirror/codemirror_loadmode.js', |
@@ -79,7 +79,8 b' class SearchController(BaseRepoControlle' | |||||
79 |
|
79 | |||
80 | try: |
|
80 | try: | |
81 | search_result = searcher.search( |
|
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 | formatted_results = Page( |
|
85 | formatted_results = Page( | |
85 | search_result['results'], page=requested_page, |
|
86 | search_result['results'], page=requested_page, |
@@ -36,11 +36,14 b' import urlparse' | |||||
36 | import time |
|
36 | import time | |
37 | import string |
|
37 | import string | |
38 | import hashlib |
|
38 | import hashlib | |
|
39 | import pygments | |||
39 |
|
40 | |||
40 | from datetime import datetime |
|
41 | from datetime import datetime | |
41 | from functools import partial |
|
42 | from functools import partial | |
42 | from pygments.formatters.html import HtmlFormatter |
|
43 | from pygments.formatters.html import HtmlFormatter | |
43 | from pygments import highlight as code_highlight |
|
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 | from pylons import url |
|
47 | from pylons import url | |
45 | from pylons.i18n.translation import _, ungettext |
|
48 | from pylons.i18n.translation import _, ungettext | |
46 | from pyramid.threadlocal import get_current_request |
|
49 | from pyramid.threadlocal import get_current_request | |
@@ -307,6 +310,176 b' class CodeHtmlFormatter(HtmlFormatter):' | |||||
307 | yield 0, '</td></tr></table>' |
|
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 | def pygmentize(filenode, **kwargs): |
|
483 | def pygmentize(filenode, **kwargs): | |
311 | """ |
|
484 | """ | |
312 | pygmentize function using pygments |
|
485 | pygmentize function using pygments |
@@ -90,7 +90,8 b' class Search(BaseSearch):' | |||||
90 | if self.searcher: |
|
90 | if self.searcher: | |
91 | self.searcher.close() |
|
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 | log.debug(u'QUERY: %s on %s', query, document_type) |
|
95 | log.debug(u'QUERY: %s on %s', query, document_type) | |
95 | result = { |
|
96 | result = { | |
96 | 'results': [], |
|
97 | 'results': [], |
@@ -514,6 +514,26 b' div.search-code-body {' | |||||
514 | .match { background-color: #faffa6;} |
|
514 | .match { background-color: #faffa6;} | |
515 | .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; } |
|
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 | div.annotatediv { margin-left: 2px; margin-right: 4px; } |
|
539 | div.annotatediv { margin-left: 2px; margin-right: 4px; } |
@@ -33,7 +33,7 b'' | |||||
33 | </div> |
|
33 | </div> | |
34 | </td> |
|
34 | </td> | |
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"> |
|
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 |
|
36 | %if entry.get('message_hl'): | |
37 | ${h.literal(entry['message_hl'])} |
|
37 | ${h.literal(entry['message_hl'])} | |
38 | %else: |
|
38 | %else: | |
39 | ${h.urlify_commit_message(entry['message'], entry['repository'])} |
|
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 | <div class="search-results"> |
|
38 | <div class="search-results"> | |
2 | %for entry in c.formatted_results: |
|
39 | %for entry in c.formatted_results: | |
3 | ## search results are additionally filtered, and this check is just a safe gate |
|
40 | ## search results are additionally filtered, and this check is just a safe gate | |
@@ -29,7 +66,7 b'' | |||||
29 | <div class="buttons"> |
|
66 | <div class="buttons"> | |
30 | <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',''))}"> |
|
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 | ${_('Show Full History')} |
|
68 | ${_('Show Full History')} | |
32 |
</a> | |
|
69 | </a> | | |
33 | ${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','')))} |
|
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 | | ${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','')))} |
|
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 | | <a href="${h.url('files_rawfile_home',repo_name=entry.get('repository',''),revision=entry.get('commit_id', 'tip'),f_path=entry.get('f_path',''))}"> |
|
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 | </div> |
|
75 | </div> | |
39 | </div> |
|
76 | </div> | |
40 | <div class="code-body search-code-body"> |
|
77 | <div class="code-body search-code-body"> | |
41 | <pre>${h.literal(entry['content_short_hl'])}</pre> |
|
78 | ${highlight_text_file(c.cur_query, entry['content'], | |
42 | </div> |
|
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 | </div> |
|
82 | </div> | |
44 | % endif |
|
83 | % endif | |
45 | %endfor |
|
84 | %endfor | |
@@ -49,3 +88,14 b'' | |||||
49 | ${c.formatted_results.pager('$link_previous ~2~ $link_next')} |
|
88 | ${c.formatted_results.pager('$link_previous ~2~ $link_next')} | |
50 | </div> |
|
89 | </div> | |
51 | %endif |
|
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 | def test_chop_at(test_text, inclusive, expected_text): |
|
155 | def test_chop_at(test_text, inclusive, expected_text): | |
156 | assert helpers.chop_at_smart( |
|
156 | assert helpers.chop_at_smart( | |
157 | test_text, '\n', inclusive, '...') == expected_text |
|
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