OSDN Git Service

BugTrack/2433 Control Search Request interval
[pukiwiki/pukiwiki.git] / skin / search2.js
1 // PukiWiki - Yet another WikiWikiWeb clone.
2 // search2.js
3 // Copyright
4 //   2017 PukiWiki Development Team
5 // License: GPL v2 or (at your option) any later version
6 //
7 // PukiWiki search2 pluign - JavaScript client script
8 window.addEventListener && window.addEventListener('DOMContentLoaded', function() {
9   function enableSearch2() {
10     var aroundLines = 2;
11     var maxResultLines = 20;
12     var minBlockLines = 5;
13     var minSearchWaitMilliseconds = 100;
14     function escapeHTML (s) {
15       if(typeof s !== 'string') {
16         s = '' + s;
17       }
18       return s.replace(/[&"<>]/g, function(m) {
19         return {
20           '&': '&amp;',
21           '"': '&quot;',
22           '<': '&lt;',
23           '>': '&gt;',
24         }[m];
25       });
26     }
27     function doSearch(searchText, session, startIndex) {
28       var url = './?cmd=search2&action=query';
29       if (searchText) {
30         url += '&q=' + encodeURIComponent(searchText);
31       }
32       url += '&start=' + startIndex;
33       fetch (url
34       ).then(function(response){
35         if (response.ok) {
36           return response.json();
37         } else {
38           throw new Error(response.status + ': ' +
39             + response.statusText + ' on ' + url);
40         }
41       }).then(function(obj) {
42         showResult(obj, session, searchText);
43       })['catch'](function(err){
44         console.log(err);
45         console.log('Error! Please check JavaScript console' + '\n' + JSON.stringify(err) + '|' + err);
46       });
47     }
48     function getMessageTemplate(idText, defaultText) {
49       var messageHolder = document.querySelector('#' + idText);
50       var messageTemplate = (messageHolder && messageHolder.value) || defaultText;
51       return messageTemplate;
52     }
53     function showResult(obj, session, searchText) {
54       var searchRegex = textToRegex(searchText);
55       var ul = document.querySelector('#result-list');
56       if (!ul) return;
57       if (obj.start_index === 0) {
58         ul.innerHTML = '';
59       }
60       if (! session.scan_page_count) session.scan_page_count = 0;
61       if (! session.read_page_count) session.read_page_count = 0;
62       if (! session.hit_page_count) session.hit_page_count = 0;
63       session.scan_page_count += obj.scan_page_count;
64       session.read_page_count += obj.read_page_count;
65       session.hit_page_count += obj.results.length;
66       session.page_count = obj.page_count;
67
68       var msg = obj.message;
69       var notFoundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_notfound',
70         'No page which contains $1 has been found.');
71       var foundMessageTemplate = getMessageTemplate('_plugin_search2_msg_result_found',
72         'In the page <strong>$2</strong>, <strong>$3</strong> pages that contain all the terms $1 were found.');
73       var searchTextDecorated = findAndDecorateText(searchText, searchRegex);
74       if (searchTextDecorated === null) searchTextDecorated = escapeHTML(searchText);
75       var messageTemplate = foundMessageTemplate;
76       if (obj.search_done && session.hit_page_count === 0) {
77         messageTemplate = notFoundMessageTemplate;
78       }
79       msg = messageTemplate.replace(/\$1|\$2|\$3/g, function(m){
80         return {
81           '$1': searchTextDecorated,
82           '$2': session.hit_page_count,
83           '$3': session.read_page_count
84         }[m];
85       });
86       document.querySelector('#_plugin_search2_message').innerHTML = msg;
87
88       if (obj.search_done) {
89         setSearchStatus('');
90       } else {
91         var progress = ' (' + session.read_page_count + ' / ' +
92           session.scan_page_count + ' / ' + session.page_count + ')';
93         var e = document.querySelector('#_plugin_search2_msg_searching');
94         var msg = e && e.value || 'Searching...';
95         setSearchStatus(msg + progress);
96       }
97       var results = obj.results;
98       results.forEach(function(val, index) {
99         var fragment = document.createDocumentFragment();
100         var li = document.createElement('li');
101         var href = val.url;
102         var decoratedName = findAndDecorateText(val.name, searchRegex);
103         if (! decoratedName) {
104           decoratedName = escapeHTML(val.name);
105         }
106         li.innerHTML = '<a href="' + href + '">' + decoratedName + '</a>';
107         fragment.appendChild(li);
108         var summary = getSummary(val.body, searchRegex);
109         for (var i = 0; i < summary.length; i++) {
110           var pre = document.createElement('pre');
111           pre.innerHTML = summary[i].lines.join('\n');
112           fragment.appendChild(pre);
113         }
114         ul.appendChild(fragment);
115       });
116       if (!obj.search_done && obj.next_start_index) {
117         var waitE = document.querySelector('#_search2_search_wait_milliseconds');
118         var interval = minSearchWaitMilliseconds;
119         try {
120           interval = parseInt(waitE.value);
121         } catch (e) {
122           interval = minSearchWaitMilliseconds;
123         }
124         if (interval < minSearchWaitMilliseconds) {
125           interval = minSearchWaitMilliseconds;
126         }
127         setTimeout(function(){
128           doSearch(searchText, session, obj.next_start_index);
129         }, interval);
130       }
131     }
132     function textToRegex(searchText) {
133       var regEscape = /[\\^$.*+?()[\]{}|]/g;
134       var s1 = searchText.replace(/^\s+|\s+$/g, '');
135       var sp = s1.split(/\s+/);
136       var rText = '';
137       for (var i = 0; i < sp.length; i++) {
138         if (rText !== '') {
139           rText += '|'
140         }
141         rText += '(' + sp[i].replace(regEscape, '\\$&') + ')';
142       }
143       return new RegExp(rText, 'ig');
144     }
145     function getTargetLines(body, searchRegex) {
146       var lines = body.split('\n');
147       var found = [];
148       var foundLines = [];
149       var isInAuthorHeader = true;
150       var lastFoundLineIndex = -1 - aroundLines;
151       var lastAddedLineIndex = lastFoundLineIndex;
152       var blocks = [];
153       var lineCount = 0;
154       for (var index = 0, length = lines.length; index < length; index++) {
155         var line = lines[index];
156         if (isInAuthorHeader) {
157           // '#author line is not search target'
158           if (line.match(/^#author\(/)) {
159             // Remove this line from search target
160             continue;
161           } else if (line.match(/^#freeze(\W|$)/)) {
162             // Stil in header
163           } else {
164             // Already in body
165             isInAuthorHeader = false;
166           }
167         }
168         var decorated = findAndDecorateText(line, searchRegex);
169         if (decorated === null) {
170           if (index < lastFoundLineIndex + aroundLines + 1) {
171             foundLines.push('' + (index + 1) + ':\t' + escapeHTML(lines[index]));
172             lineCount++;
173             lastAddedLineIndex = index;
174           }
175         } else {
176           var startIndex = Math.max(Math.max(lastAddedLineIndex + 1, index - aroundLines), 0);
177           if (lastAddedLineIndex + 1 < startIndex) {
178             // Newly found!
179             var block = {
180               startIndex: startIndex,
181               foundLineIndex: index,
182               lines: []
183             };
184             foundLines = block.lines;
185             blocks.push(block);
186           }
187           if (lineCount >= maxResultLines) {
188             foundLines.push('...');
189             return blocks;
190           }
191           for (var i = startIndex; i < index; i++) {
192             foundLines.push('' + (i + 1) + ':\t' + escapeHTML(lines[i]));
193             lineCount++;
194           }
195           foundLines.push('' + (index + 1) + ':\t' + decorated);
196           lineCount++;
197           lastFoundLineIndex = lastAddedLineIndex = index;
198         }
199       }
200       return blocks;
201       //return foundLines.join('\n');
202     }
203     function findAndDecorateText(text, searchRegex) {
204       var isReplaced = false;
205       var lastIndex = 0;
206       var m;
207       var decorated = '';
208       searchRegex.lastIndex = 0;
209       while ((m = searchRegex.exec(text)) !== null) {
210         isReplaced = true;
211         var pre = text.substring(lastIndex, m.index);
212         decorated += escapeHTML(pre);
213         for (var i = 1; i < m.length; i++) {
214           if (m[i]) {
215             decorated += '<strong class="word' + (i - 1) + '">' + escapeHTML(m[i]) + '</strong>'
216           }
217         }
218         lastIndex = searchRegex.lastIndex;
219       }
220       if (isReplaced) {
221         decorated += escapeHTML(text.substr(lastIndex));
222         return decorated;
223       }
224       return null;
225     }
226     function getSummary(bodyText, searchRegex) {
227       return getTargetLines(bodyText, searchRegex);
228     }
229     function hookSearch2(e) {
230       var form = document.querySelector('form');
231       if (form && form.q) {
232         var q = form.q;
233         if (q.value === '') {
234           q.focus();
235         }
236       }
237     }
238     function removeEncodeHint() {
239       var form = document.querySelector('form');
240       if (form && form.encode_hint && (typeof form.encode_hint.removeAttribute === 'function')) {
241         form.encode_hint.removeAttribute('name');
242       }
243     }
244     function kickFirstSearch() {
245       var searchText = document.querySelector('#_plugin_search2_searchtext');
246       if (!searchText) return;
247       if (searchText && searchText.value) {
248         var e = document.querySelector('#_plugin_search2_msg_searching');
249         var msg = e && e.value || 'Searching...';
250         setSearchStatus(msg);
251         doSearch(searchText.value, {}, 0);
252       }
253     }
254     function setSearchStatus(statusText) {
255       var statusObj = document.querySelector('#_plugin_search2_search_status');
256       if (statusObj) {
257         statusObj.textContent = statusText;
258       }
259     }
260     function replaceSearchWithSearch2() {
261       var forms = document.querySelectorAll('form');
262       for (var i = 0; i < forms.length; i++) {
263         var f = forms[i];
264         if (f.action.match(/cmd=search$/)) {
265           f.addEventListener('submit', function(e) {
266             var q = e.target.word.value;
267             var base = e.target && e.target.base && e.target.base.value;
268             var loc = document.location;
269             var url = loc.protocol + '//' + loc.host + loc.pathname +
270               '?cmd=search2' +
271               (base ? '&base=' + encodeURIComponent(base) : '') +
272               '&q=' + encodeURIComponent(q);
273             e.preventDefault();
274             setTimeout(function() {
275               location.href = url;
276             }, 1);
277             return false;
278           });
279         }
280       }
281     }
282     function isEnabledFunctions() {
283       if (window.fetch && document.querySelector) {
284         return true;
285       }
286       return false;
287     }
288     if (! isEnabledFunctions()) return;
289     replaceSearchWithSearch2();
290     hookSearch2();
291     removeEncodeHint();
292     kickFirstSearch();
293   }
294   enableSearch2();
295 });