]> BookStack Code Mirror - website/blob - themes/bookstack/static/libs/webidx.js
Started playing with webidx based search
[website] / themes / bookstack / static / libs / webidx.js
1 // Taken from https://github.com/gbxyz/webidx/tree/main
2 // BSD 3-Clause License
3 // Copyright (c) 2024, Gavin Brown
4 // Full license: https://github.com/gbxyz/webidx/blob/a28a984d38fd546d1bec4d6a4a5a47ab86cb08f8/LICENSE
5
6 window.webidx = {};
7 webidx = window.webidx;
8
9 webidx.search = async function (params) {
10   if (!webidx.sql) {
11     //
12     // initialise sql.js
13     //
14     webidx.sql = await window.initSqlJs({locateFile: file => `https://sql.js.org/dist/${file}`});
15   }
16
17   if (webidx.hasOwnProperty('db')) {
18     webidx.displayResults(webidx.query(params.query), params);
19
20   } else {
21     webidx.loadDB(params);
22
23   }
24 };
25
26 webidx.loadDB = function (params) {
27   var xhr = new XMLHttpRequest();
28
29   xhr.open('GET', params.dbfile);
30   xhr.timeout = params.timeout ?? 5000;
31   xhr.responseType = 'arraybuffer';
32
33   xhr.ontimeout = function() {
34     if (params.hasOwnProperty('errorCallback')) {
35       params.errorCallback('Unable to load index, please refresh the page.');
36     }
37   };
38
39   xhr.onload = function() {
40     webidx.initializeDB(this.response);
41     webidx.displayResults(webidx.query(params.query), params);
42   };
43
44   xhr.send();
45 };
46
47 webidx.initializeDB = function (arrayBuffer) {
48   webidx.db = new webidx.sql.Database(new Uint8Array(arrayBuffer));
49
50   //
51   // prepare statements
52   //
53   webidx.wordQuery  = webidx.db.prepare("SELECT `id` FROM `words` WHERE (`word`=:word)");
54   webidx.idxQuery   = webidx.db.prepare("SELECT `page_id` FROM `index` WHERE (`word`=:word)");
55   webidx.pageQuery  = webidx.db.prepare("SELECT `url`,`title` FROM `pages` WHERE (`id`=:id)");
56 };
57
58 webidx.getWordID = function (word) {
59   webidx.wordQuery.bind([word]);
60   webidx.wordQuery.step();
61   var word_id = webidx.wordQuery.get().shift();
62
63   webidx.wordQuery.reset();
64
65   return word_id;
66 };
67
68 webidx.getPagesHavingWord = function (word_id) {
69   var pages = [];
70
71   webidx.idxQuery.bind([word_id]);
72
73   while (webidx.idxQuery.step()) {
74     pages.push(webidx.idxQuery.get().shift());
75   }
76
77   webidx.idxQuery.reset();
78
79   return pages;
80 };
81
82 webidx.getPage = function (page_id) {
83   webidx.pageQuery.bind([page_id]);
84
85   webidx.pageQuery.step();
86
87   var page = webidx.pageQuery.getAsObject();
88
89   webidx.pageQuery.reset();
90
91   return page;
92 };
93
94 webidx.query = function (query) {
95   //
96   // split the search term into words
97   //
98   var words = query.toLowerCase().split(" ");
99
100   //
101   // this array maps page ID to rank
102   //
103   var pageRank = [];
104
105   //
106   // iterate over each word
107   //
108   while (words.length > 0) {
109     var word = words.shift();
110
111     var invert = false;
112     if (0 == word.indexOf("-")) {
113       invert = true;
114       word = word.substring(1);
115     }
116
117     var word_id = webidx.getWordID(word);
118
119     //
120     // if the word isn't present, ignore it
121     //
122     if (word_id) {
123       var pages = webidx.getPagesHavingWord(word_id);
124
125       pages.forEach(function (page_id) {
126         if (invert) {
127           if (pageRank[page_id]) {
128             pageRank[page_id] -= 65535;
129
130           } else {
131             pageRank[page_id] = -65535;
132             
133           }
134
135         } else {
136           if (pageRank[page_id]) {
137             pageRank[page_id]++;
138
139           } else {
140             pageRank[page_id] = 1;
141
142           }
143         }
144       });
145     }
146   }
147
148   //
149   // transform the results into a format that can be sorted
150   //
151   var sortedPages = [];
152
153   pageRank.forEach(function (rank, page_id) {
154     if (rank > 0) {
155       sortedPages.push({rank: rank, page_id: page_id});
156     }
157   })
158
159   //
160   // sort the results in descending rank order
161   //
162   sortedPages.sort(function(a, b) {
163     return b.rank - a.rank;
164   });
165
166   //
167   // this will be populated with the actual pages
168   //
169   var pages = [];
170
171   //
172   // get page data for each result
173   //
174   sortedPages.forEach(function(result) {
175     pages.push(webidx.getPage(result.page_id));
176   });
177
178   return pages;
179 };
180
181 webidx.regExpQuote = function (str) {
182   return str.replace(/[/\-\\^$*+?.()|[\]{}]/g, '\\$&');
183 };
184
185 webidx.displayResults = function (pages, params) {
186   var callback = params.resultCallback ?? webidx.displayDialog;
187   callback(pages, params);
188 };
189
190 webidx.displayDialog = function (pages, params) {
191   var dialog = document.createElement('dialog');
192   dialog.classList.add('webidx-results-dialog')
193
194   dialog.appendChild(document.createElement('h2')).appendChild(document.createTextNode('Search Results'));
195
196   if (pages.length < 1) {
197     dialog.appendChild(document.createElement('p')).appendChild(document.createTextNode('Nothing found.'));
198
199   } else {
200     var ul = dialog.appendChild(document.createElement('ul'));
201
202     pages.forEach(function(page) {
203       var titleText = page.title;
204
205       if (params.titleSuffix) {
206         titleText = titleText.replace(new RegExp(webidx.regExpQuote(params.titleSuffix)+'$'), '');
207       }
208
209       if (params.titlePrefix) {
210         titleText = titleText.replace(new RegExp('^' + webidx.regExpQuote(params.titleSuffix)), '');
211       }
212
213       var li = ul.appendChild(document.createElement('li'));
214       var a = li.appendChild(document.createElement('a'));
215       a.setAttribute('href', page.url);
216       a.appendChild(document.createTextNode(titleText));
217       li.appendChild(document.createElement('br'));
218
219       var span = li.appendChild(document.createElement('span'));
220       span.classList.add('webidx-page-url');
221       span.appendChild(document.createTextNode(page.url));
222     });
223   }
224
225   var form = dialog.appendChild(document.createElement('form'));
226   form.setAttribute('method', 'dialog');
227
228   var button = form.appendChild(document.createElement('button'));
229   button.setAttribute('autofocus', true);
230   button.appendChild(document.createTextNode('Close'));
231
232   document.body.appendChild(dialog);
233
234   dialog.addEventListener('close', function() {
235     dialog.parentNode.removeChild(dialog);
236   });
237
238   dialog.showModal();
239   dialog.scrollTop = 0;
240 };