PageSourceSearch

https://cmajor.dev/assets/js/just-the-docs.js

js cmajor.dev collected 2026-10-04 00:24:48 UTC 15,087 bytes, 457 lines download raw bytes

1(function (jtd, undefined) {
2
3// Event handling
4
5jtd.addEvent = function(el, type, handler) {
6  if (el.attachEvent) el.attachEvent('on'+type, handler); else el.addEventListener(type, handler);
7}
8jtd.removeEvent = function(el, type, handler) {
9  if (el.detachEvent) el.detachEvent('on'+type, handler); else el.removeEventListener(type, handler);
10}
11jtd.onReady = function(ready) {
12  // in case the document is already rendered
13  if (document.readyState!='loading') ready();
14  // modern browsers
15  else if (document.addEventListener) document.addEventListener('DOMContentLoaded', ready);
16  // IE <= 8
17  else document.attachEvent('onreadystatechange', function(){
18      if (document.readyState=='complete') ready();
19  });
20}
21
22// Show/hide mobile menu
23
24function initNav() {
25  jtd.addEvent(document, 'click', function(e){
26    var target = e.target;
27    while (target && !(target.classList && target.classList.contains('nav-list-expander'))) {
28      target = target.parentNode;
29    }
30    if (target) {
31      e.preventDefault();
32      target.parentNode.classList.toggle('active');
33    }
34  });
35
36  const siteNav = document.getElementById('site-nav');
37  const mainHeader = document.getElementById('main-header');
38  const menuButton = document.getElementById('menu-button');
39
40  jtd.addEvent(menuButton, 'click', function(e){
41    e.preventDefault();
42
43    if (menuButton.classList.toggle('nav-open')) {
44      siteNav.classList.add('nav-open');
45      mainHeader.classList.add('nav-open');
46    } else {
47      siteNav.classList.remove('nav-open');
48      mainHeader.classList.remove('nav-open');
49    }
50  });
51}
52// Site search
53
54function initSearch() {
55  var request = new XMLHttpRequest();
56  request.open('GET', 'https://cmajor.dev/assets/js/search-data.json', true);
57
58  request.onload = function(){
59    if (request.status >= 200 && request.status < 400) {
60      var docs = JSON.parse(request.responseText);
61      
62      lunr.tokenizer.separator = /[\s/]+/
63
64      var index = lunr(function(){
65        this.ref('id');
66        this.field('title', { boost: 200 });
67        this.field('content', { boost: 2 });
68        this.field('relUrl');
69        this.metadataWhitelist = ['position']
70
71        for (var i in docs) {
72          this.add({
73            id: i,
74            title: docs[i].title,
75            content: docs[i].content,
76            relUrl: docs[i].relUrl
77          });
78        }
79      });
80
81      searchLoaded(index, docs);
82    } else {
83      console.log('Error loading ajax request. Request status:' + request.status);
84    }
85  };
86
87  request.onerror = function(){
88    console.log('There was a connection error');
89  };
90
91  request.send();
92}
93
94function searchLoaded(index, docs) {
95  var index = index;
96  var docs = docs;
97  var searchInput = document.getElementById('search-input');
98  var searchResults = document.getElementById('search-results');
99  var mainHeader = document.getElementById('main-header');
100  var currentInput;
101  var currentSearchIndex = 0;
102
103  function showSearch() {
104    document.documentElement.classList.add('search-active');
105  }
106
107  function hideSearch() {
108    document.documentElement.classList.remove('search-active');
109  }
110
111  function update() {
112    currentSearchIndex++;
113
114    var input = searchInput.value;
115    if (input === '') {
116      hideSearch();
117    } else {
118      showSearch();
119      // scroll search input into view, workaround for iOS Safari
120      window.scroll(0, -1);
121      setTimeout(function(){ window.scroll(0, 0); }, 0);
122    }
123    if (input === currentInput) {
124      return;
125    }
126    currentInput = input;
127    searchResults.innerHTML = '';
128    if (input === '') {
129      return;
130    }
131
132    var results = index.query(function (query) {
133      var tokens = lunr.tokenizer(input)
134      query.term(tokens, {
135        boost: 10
136      });
137      query.term(tokens, {
138        wildcard: lunr.Query.wildcard.TRAILING
139      });
140    });
141
142    if ((results.length == 0) && (input.length > 2)) {
143      var tokens = lunr.tokenizer(input).filter(function(token, i) {
144        return token.str.length < 20;
145      })
146      if (tokens.length > 0) {
147        results = index.query(function (query) {
148          query.term(tokens, {
149            editDistance: Math.round(Math.sqrt(input.length / 2 - 1))
150          });
151        });
152      }
153    }
154
155    if (results.length == 0) {
156      var noResultsDiv = document.createElement('div');
157      noResultsDiv.classList.add('search-no-result');
158      noResultsDiv.innerText = 'No results found';
159      searchResults.appendChild(noResultsDiv);
160
161    } else {
162      var resultsList = document.createElement('ul');
163      resultsList.classList.add('search-results-list');
164      searchResults.appendChild(resultsList);
165
166      addResults(resultsList, results, 0, 10, 100, currentSearchIndex);
167    }
168
169    function addResults(resultsList, results, start, batchSize, batchMillis, searchIndex) {
170      if (searchIndex != currentSearchIndex) {
171        return;
172      }
173      for (var i = start; i < (start + batchSize); i++) {
174        if (i == results.length) {
175          return;
176        }
177        addResult(resultsList, results[i]);
178      }
179      setTimeout(function() {
180        addResults(resultsList, results, start + batchSize, batchSize, batchMillis, searchIndex);
181      }, batchMillis);
182    }
183
184    function addResult(resultsList, result) {
185      var doc = docs[result.ref];
186
187      var resultsListItem = document.createElement('li');
188      resultsListItem.classList.add('search-results-list-item');
189      resultsList.appendChild(resultsListItem);
190
191      var resultLink = document.createElement('a');
192      resultLink.classList.add('search-result');
193      resultLink.setAttribute('href', doc.url);
194      resultsListItem.appendChild(resultLink);
195
196      var resultTitle = document.createElement('div');
197      resultTitle.classList.add('search-result-title');
198      resultLink.appendChild(resultTitle);
199
200      var resultDoc = document.createElement('div');
201      resultDoc.classList.add('search-result-doc');
202      resultDoc.innerHTML = '<svg viewBox="0 0 24 24" class="search-result-icon"><use xlink:href="#svg-doc"></use></svg>';
203      resultTitle.appendChild(resultDoc);
204
205      var resultDocTitle = document.createElement('div');
206      resultDocTitle.classList.add('search-result-doc-title');
207      resultDocTitle.innerHTML = doc.doc;
208      resultDoc.appendChild(resultDocTitle);
209      var resultDocOrSection = resultDocTitle;
210
211      if (doc.doc != doc.title) {
212        resultDoc.classList.add('search-result-doc-parent');
213        var resultSection = document.createElement('div');
214        resultSection.classList.add('search-result-section');
215        resultSection.innerHTML = doc.title;
216        resultTitle.appendChild(resultSection);
217        resultDocOrSection = resultSection;
218      }
219
220      var metadata = result.matchData.metadata;
221      var titlePositions = [];
222      var contentPositions = [];
223      for (var j in metadata) {
224        var meta = metadata[j];
225        if (meta.title) {
226          var positions = meta.title.position;
227          for (var k in positions) {
228            titlePositions.push(positions[k]);
229          }
230        }
231        if (meta.content) {
232          var positions = meta.content.position;
233          for (var k in positions) {
234            var position = positions[k];
235            var previewStart = position[0];
236            var previewEnd = position[0] + position[1];
237            var ellipsesBefore = true;
238            var ellipsesAfter = true;
239            for (var k = 0; k < 3; k++) {
240              var nextSpace = doc.content.lastIndexOf(' ', previewStart - 2);
241              var nextDot = doc.content.lastIndexOf('. ', previewStart - 2);
242              if ((nextDot >= 0) && (nextDot > nextSpace)) {
243                previewStart = nextDot + 1;
244                ellipsesBefore = false;
245                break;
246              }
247              if (nextSpace < 0) {
248                previewStart = 0;
249                ellipsesBefore = false;
250                break;
251              }
252              previewStart = nextSpace + 1;
253            }
254            for (var k = 0; k < 3; k++) {
255              var nextSpace = doc.content.indexOf(' ', previewEnd + 1);
256              var nextDot = doc.content.indexOf('. ', previewEnd + 1);
257              if ((nextDot >= 0) && (nextDot < nextSpace)) {
258                previewEnd = nextDot;
259                ellipsesAfter = false;
260                break;
261              }
262              if (nextSpace < 0) {
263                previewEnd = doc.content.length;
264                ellipsesAfter = false;
265                break;
266              }
267              previewEnd = nextSpace;
268            }
269            contentPositions.push({
270              highlight: position,
271              previewStart: previewStart, previewEnd: previewEnd,
272              ellipsesBefore: ellipsesBefore, ellipsesAfter: ellipsesAfter
273            });
274          }
275        }
276      }
277
278      if (titlePositions.length > 0) {
279        titlePositions.sort(function(p1, p2){ return p1[0] - p2[0] });
280        resultDocOrSection.innerHTML = '';
281        addHighlightedText(resultDocOrSection, doc.title, 0, doc.title.length, titlePositions);
282      }
283
284      if (contentPositions.length > 0) {
285        contentPositions.sort(function(p1, p2){ return p1.highlight[0] - p2.highlight[0] });
286        var contentPosition = contentPositions[0];
287        var previewPosition = {
288          highlight: [contentPosition.highlight],
289          previewStart: contentPosition.previewStart, previewEnd: contentPosition.previewEnd,
290          ellipsesBefore: contentPosition.ellipsesBefore, ellipsesAfter: contentPosition.ellipsesAfter
291        };
292        var previewPositions = [previewPosition];
293        for (var j = 1; j < contentPositions.length; j++) {
294          contentPosition = contentPositions[j];
295          if (previewPosition.previewEnd < contentPosition.previewStart) {
296            previewPosition = {
297              highlight: [contentPosition.highlight],
298              previewStart: contentPosition.previewStart, previewEnd: contentPosition.previewEnd,
299              ellipsesBefore: contentPosition.ellipsesBefore, ellipsesAfter: contentPosition.ellipsesAfter
300            }
301            previewPositions.push(previewPosition);
302          } else {
303            previewPosition.highlight.push(contentPosition.highlight);
304            previewPosition.previewEnd = contentPosition.previewEnd;
305            previewPosition.ellipsesAfter = contentPosition.ellipsesAfter;
306          }
307        }
308
309        var resultPreviews = document.createElement('div');
310        resultPreviews.classList.add('search-result-previews');
311        resultLink.appendChild(resultPreviews);
312
313        var content = doc.content;
314        for (var j = 0; j < Math.min(previewPositions.length, 2); j++) {
315          var position = previewPositions[j];
316
317          var resultPreview = document.createElement('div');
318          resultPreview.classList.add('search-result-preview');
319          resultPreviews.appendChild(resultPreview);
320
321          if (position.ellipsesBefore) {
322            resultPreview.appendChild(document.createTextNode('... '));
323          }
324          addHighlightedText(resultPreview, content, position.previewStart, position.previewEnd, position.highlight);
325          if (position.ellipsesAfter) {
326            resultPreview.appendChild(document.createTextNode(' ...'));
327          }
328        }
329      }
330      var resultRelUrl = document.createElement('span');
331      resultRelUrl.classList.add('search-result-rel-url');
332      resultRelUrl.innerText = doc.relUrl;
333      resultTitle.appendChild(resultRelUrl);
334    }
335
336    function addHighlightedText(parent, text, start, end, positions) {
337      var index = start;
338      for (var i in positions) {
339        var position = positions[i];
340        var span = document.createElement('span');
341        span.innerHTML = text.substring(index, position[0]);
342        parent.appendChild(span);
343        index = position[0] + position[1];
344        var highlight = document.createElement('span');
345        highlight.classList.add('search-result-highlight');
346        highlight.innerHTML = text.substring(position[0], index);
347        parent.appendChild(highlight);
348      }
349      var span = document.createElement('span');
350      span.innerHTML = text.substring(index, end);
351      parent.appendChild(span);
352    }
353  }
354
355  jtd.addEvent(searchInput, 'focus', function(){
356    setTimeout(update, 0);
357  });
358
359  jtd.addEvent(searchInput, 'keyup', function(e){
360    switch (e.keyCode) {
361      case 27: // When esc key is pressed, hide the results and clear the field
362        searchInput.value = '';
363        break;
364      case 38: // arrow up
365      case 40: // arrow down
366      case 13: // enter
367        e.preventDefault();
368        return;
369    }
370    update();
371  });
372
373  jtd.addEvent(searchInput, 'keydown', function(e){
374    switch (e.keyCode) {
375      case 38: // arrow up
376        e.preventDefault();
377        var active = document.querySelector('.search-result.active');
378        if (active) {
379          active.classList.remove('active');
380          if (active.parentElement.previousSibling) {
381            var previous = active.parentElement.previousSibling.querySelector('.search-result');
382            previous.classList.add('active');
383          }
384        }
385        return;
386      case 40: // arrow down
387        e.preventDefault();
388        var active = document.querySelector('.search-result.active');
389        if (active) {
390          if (active.parentElement.nextSibling) {
391            var next = active.parentElement.nextSibling.querySelector('.search-result');
392            active.classList.remove('active');
393            next.classList.add('active');
394          }
395        } else {
396          var next = document.querySelector('.search-result');
397          if (next) {
398            next.classList.add('active');
399          }
400        }
401        return;
402      case 13: // enter
403        e.preventDefault();
404        var active = document.querySelector('.search-result.active');
405        if (active) {
406          active.click();
407        } else {
408          var first = document.querySelector('.search-result');
409          if (first) {
410            first.click();
411          }
412        }
413        return;
414    }
415  });
416
417  jtd.addEvent(document, 'click', function(e){
418    if (e.target != searchInput) {
419      hideSearch();
420    }
421  });
422}
423
424// Switch theme
425
426jtd.getTheme = function() {
427  var cssFileHref = document.querySelector('[rel="stylesheet"]').getAttribute('href');
428  return cssFileHref.substring(cssFileHref.lastIndexOf('-') + 1, cssFileHref.length - 4);
429}
430
431jtd.setTheme = function(theme) {
432  var cssFile = document.querySelector('[rel="stylesheet"]');
433  cssFile.setAttribute('href', 'https://cmajor.dev/assets/css/just-the-docs-' + theme + '.css');
434}
435
436// Document ready
437
438jtd.onReady(function(){
439  initNav();
440  initSearch();
441});
442
443})(window.jtd = window.jtd || {});
444
445// jquerify.js
446// Add jQuery to any page that does not have it already.
447
448(function () {
449
450    if ( !window.jQuery ) {
451      var s = document.createElement('script');
452      s.setAttribute('src', '//ajax.googleapis.com/ajax/libs/jquery/2.0.3/jquery.min.js');
453      document.body?.appendChild(s);
454      console.log('jquery loaded!');
455    }
456
457  })();

Line numbers count LF bytes from the start of the resource, as the search results do. Vendor segments are library code the classifier recognised; they are stored but not indexed. Bytes are shown as Latin1 characters, one per byte.