From 790897184c1dda158048d5c5e8de5f1e202394a5 Mon Sep 17 00:00:00 2001
From: "Uwe Jandt (DESY, HIFIS)" <uwe.jandt@desy.de>
Date: Wed, 29 Sep 2021 14:25:03 +0200
Subject: [PATCH] Simple search function

---
 _data/navigation.yml             |   3 +
 _plugins/simple_search_filter.rb |  19 ++
 js/simple-jekyll-search.js       | 434 +++++++++++++++++++++++++++++++
 search.json                      |  29 +++
 search.md                        |  46 ++++
 5 files changed, 531 insertions(+)
 create mode 100644 _plugins/simple_search_filter.rb
 create mode 100644 js/simple-jekyll-search.js
 create mode 100644 search.json
 create mode 100644 search.md

diff --git a/_data/navigation.yml b/_data/navigation.yml
index e50f747d4..a422db1df 100644
--- a/_data/navigation.yml
+++ b/_data/navigation.yml
@@ -35,6 +35,9 @@ header:
         - title: Jobs <i class="fas fa-bell"></i>
           url: "job_offers.html"
 
+        - title: <i class="fas fa-search"></i>
+          url: "search.html"
+
 footer:
          - title: Contact
            url: "contact.html"
diff --git a/_plugins/simple_search_filter.rb b/_plugins/simple_search_filter.rb
new file mode 100644
index 000000000..29d2869fb
--- /dev/null
+++ b/_plugins/simple_search_filter.rb
@@ -0,0 +1,19 @@
+module Jekyll
+  module CharFilter
+    def remove_chars(input)
+      input.gsub! '\\','&#92;'
+      input.gsub! /\t/, '    '
+      input.strip_control_and_extended_characters
+    end
+  end
+end
+
+Liquid::Template.register_filter(Jekyll::CharFilter)
+
+class String
+  def strip_control_and_extended_characters()
+    chars.each_with_object("") do |char, str|
+      str << char if char.ascii_only? and char.ord.between?(32,126)
+    end
+  end
+end
diff --git a/js/simple-jekyll-search.js b/js/simple-jekyll-search.js
new file mode 100644
index 000000000..cfbb66b0a
--- /dev/null
+++ b/js/simple-jekyll-search.js
@@ -0,0 +1,434 @@
+/*!
+  * Simple-Jekyll-Search
+  * Copyright 2015-2020, Christian Fei
+  * Licensed under the MIT License.
+  * https://github.com/christian-fei/Simple-Jekyll-Search
+  */
+
+(function(){
+'use strict'
+
+var _$Templater_7 = {
+  compile: compile,
+  setOptions: setOptions
+}
+
+const options = {}
+options.pattern = /\{(.*?)\}/g
+options.template = ''
+options.middleware = function () {}
+
+function setOptions (_options) {
+  options.pattern = _options.pattern || options.pattern
+  options.template = _options.template || options.template
+  if (typeof _options.middleware === 'function') {
+    options.middleware = _options.middleware
+  }
+}
+
+function compile (data) {
+  return options.template.replace(options.pattern, function (match, prop) {
+    const value = options.middleware(prop, data[prop], options.template)
+    if (typeof value !== 'undefined') {
+      return value
+    }
+    return data[prop] || match
+  })
+}
+
+'use strict';
+
+function fuzzysearch (needle, haystack) {
+  var tlen = haystack.length;
+  var qlen = needle.length;
+  if (qlen > tlen) {
+    return false;
+  }
+  if (qlen === tlen) {
+    return needle === haystack;
+  }
+  outer: for (var i = 0, j = 0; i < qlen; i++) {
+    var nch = needle.charCodeAt(i);
+    while (j < tlen) {
+      if (haystack.charCodeAt(j++) === nch) {
+        continue outer;
+      }
+    }
+    return false;
+  }
+  return true;
+}
+
+var _$fuzzysearch_1 = fuzzysearch;
+
+'use strict'
+
+/* removed: const _$fuzzysearch_1 = require('fuzzysearch') */;
+
+var _$FuzzySearchStrategy_5 = new FuzzySearchStrategy()
+
+function FuzzySearchStrategy () {
+  this.matches = function (string, crit) {
+    return _$fuzzysearch_1(crit.toLowerCase(), string.toLowerCase())
+  }
+}
+
+'use strict'
+
+var _$LiteralSearchStrategy_6 = new LiteralSearchStrategy()
+
+function LiteralSearchStrategy () {
+  this.matches = function (str, crit) {
+    if (!str) return false
+
+    str = str.trim().toLowerCase()
+    crit = crit.trim().toLowerCase()
+
+    return crit.split(' ').filter(function (word) {
+      return str.indexOf(word) >= 0
+    }).length === crit.split(' ').length
+  }
+}
+
+'use strict'
+
+var _$Repository_4 = {
+  put: put,
+  clear: clear,
+  search: search,
+  setOptions: __setOptions_4
+}
+
+/* removed: const _$FuzzySearchStrategy_5 = require('./SearchStrategies/FuzzySearchStrategy') */;
+/* removed: const _$LiteralSearchStrategy_6 = require('./SearchStrategies/LiteralSearchStrategy') */;
+
+function NoSort () {
+  return 0
+}
+
+const data = []
+let opt = {}
+
+opt.fuzzy = false
+opt.limit = 10
+opt.searchStrategy = opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
+opt.sort = NoSort
+opt.exclude = []
+
+function put (data) {
+  if (isObject(data)) {
+    return addObject(data)
+  }
+  if (isArray(data)) {
+    return addArray(data)
+  }
+  return undefined
+}
+function clear () {
+  data.length = 0
+  return data
+}
+
+function isObject (obj) {
+  return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Object]'
+}
+
+function isArray (obj) {
+  return Boolean(obj) && Object.prototype.toString.call(obj) === '[object Array]'
+}
+
+function addObject (_data) {
+  data.push(_data)
+  return data
+}
+
+function addArray (_data) {
+  const added = []
+  clear()
+  for (let i = 0, len = _data.length; i < len; i++) {
+    if (isObject(_data[i])) {
+      added.push(addObject(_data[i]))
+    }
+  }
+  return added
+}
+
+function search (crit) {
+  if (!crit) {
+    return []
+  }
+  return findMatches(data, crit, opt.searchStrategy, opt).sort(opt.sort)
+}
+
+function __setOptions_4 (_opt) {
+  opt = _opt || {}
+
+  opt.fuzzy = _opt.fuzzy || false
+  opt.limit = _opt.limit || 10
+  opt.searchStrategy = _opt.fuzzy ? _$FuzzySearchStrategy_5 : _$LiteralSearchStrategy_6
+  opt.sort = _opt.sort || NoSort
+  opt.exclude = _opt.exclude || []
+}
+
+function findMatches (data, crit, strategy, opt) {
+  const matches = []
+  for (let i = 0; i < data.length && matches.length < opt.limit; i++) {
+    const match = findMatchesInObject(data[i], crit, strategy, opt)
+    if (match) {
+      matches.push(match)
+    }
+  }
+  return matches
+}
+
+function findMatchesInObject (obj, crit, strategy, opt) {
+  for (const key in obj) {
+    if (!isExcluded(obj[key], opt.exclude) && strategy.matches(obj[key], crit)) {
+      return obj
+    }
+  }
+}
+
+function isExcluded (term, excludedTerms) {
+  for (let i = 0, len = excludedTerms.length; i < len; i++) {
+    const excludedTerm = excludedTerms[i]
+    if (new RegExp(excludedTerm).test(term)) {
+      return true
+    }
+  }
+  return false
+}
+
+/* globals ActiveXObject:false */
+
+'use strict'
+
+var _$JSONLoader_2 = {
+  load: load
+}
+
+function load (location, callback) {
+  const xhr = getXHR()
+  xhr.open('GET', location, true)
+  xhr.onreadystatechange = createStateChangeListener(xhr, callback)
+  xhr.send()
+}
+
+function createStateChangeListener (xhr, callback) {
+  return function () {
+    if (xhr.readyState === 4 && xhr.status === 200) {
+      try {
+        callback(null, JSON.parse(xhr.responseText))
+      } catch (err) {
+        callback(err, null)
+      }
+    }
+  }
+}
+
+function getXHR () {
+  return window.XMLHttpRequest ? new window.XMLHttpRequest() : new ActiveXObject('Microsoft.XMLHTTP')
+}
+
+'use strict'
+
+var _$OptionsValidator_3 = function OptionsValidator (params) {
+  if (!validateParams(params)) {
+    throw new Error('-- OptionsValidator: required options missing')
+  }
+
+  if (!(this instanceof OptionsValidator)) {
+    return new OptionsValidator(params)
+  }
+
+  const requiredOptions = params.required
+
+  this.getRequiredOptions = function () {
+    return requiredOptions
+  }
+
+  this.validate = function (parameters) {
+    const errors = []
+    requiredOptions.forEach(function (requiredOptionName) {
+      if (typeof parameters[requiredOptionName] === 'undefined') {
+        errors.push(requiredOptionName)
+      }
+    })
+    return errors
+  }
+
+  function validateParams (params) {
+    if (!params) {
+      return false
+    }
+    return typeof params.required !== 'undefined' && params.required instanceof Array
+  }
+}
+
+'use strict'
+
+var _$utils_9 = {
+  merge: merge,
+  isJSON: isJSON
+}
+
+function merge (defaultParams, mergeParams) {
+  const mergedOptions = {}
+  for (const option in defaultParams) {
+    mergedOptions[option] = defaultParams[option]
+    if (typeof mergeParams[option] !== 'undefined') {
+      mergedOptions[option] = mergeParams[option]
+    }
+  }
+  return mergedOptions
+}
+
+function isJSON (json) {
+  try {
+    if (json instanceof Object && JSON.parse(JSON.stringify(json))) {
+      return true
+    }
+    return false
+  } catch (err) {
+    return false
+  }
+}
+
+var _$src_8 = {};
+(function (window) {
+  'use strict'
+
+  let options = {
+    searchInput: null,
+    resultsContainer: null,
+    json: [],
+    success: Function.prototype,
+    searchResultTemplate: '<li><a href="{url}" title="{desc}">{title}</a></li>',
+    templateMiddleware: Function.prototype,
+    sortMiddleware: function () {
+      return 0
+    },
+    noResultsText: 'No results found',
+    limit: 10,
+    fuzzy: false,
+    debounceTime: null,
+    exclude: []
+  }
+
+  let debounceTimerHandle
+  const debounce = function (func, delayMillis) {
+    if (delayMillis) {
+      clearTimeout(debounceTimerHandle)
+      debounceTimerHandle = setTimeout(func, delayMillis)
+    } else {
+      func.call()
+    }
+  }
+
+  const requiredOptions = ['searchInput', 'resultsContainer', 'json']
+
+  /* removed: const _$Templater_7 = require('./Templater') */;
+  /* removed: const _$Repository_4 = require('./Repository') */;
+  /* removed: const _$JSONLoader_2 = require('./JSONLoader') */;
+  const optionsValidator = _$OptionsValidator_3({
+    required: requiredOptions
+  })
+  /* removed: const _$utils_9 = require('./utils') */;
+
+  window.SimpleJekyllSearch = function (_options) {
+    const errors = optionsValidator.validate(_options)
+    if (errors.length > 0) {
+      throwError('You must specify the following required options: ' + requiredOptions)
+    }
+
+    options = _$utils_9.merge(options, _options)
+
+    _$Templater_7.setOptions({
+      template: options.searchResultTemplate,
+      middleware: options.templateMiddleware
+    })
+
+    _$Repository_4.setOptions({
+      fuzzy: options.fuzzy,
+      limit: options.limit,
+      sort: options.sortMiddleware,
+      exclude: options.exclude
+    })
+
+    if (_$utils_9.isJSON(options.json)) {
+      initWithJSON(options.json)
+    } else {
+      initWithURL(options.json)
+    }
+
+    const rv = {
+      search: search
+    }
+
+    typeof options.success === 'function' && options.success.call(rv)
+    return rv
+  }
+
+  function initWithJSON (json) {
+    _$Repository_4.put(json)
+    registerInput()
+  }
+
+  function initWithURL (url) {
+    _$JSONLoader_2.load(url, function (err, json) {
+      if (err) {
+        throwError('failed to get JSON (' + url + ')')
+      }
+      initWithJSON(json)
+    })
+  }
+
+  function emptyResultsContainer () {
+    options.resultsContainer.innerHTML = ''
+  }
+
+  function appendToResultsContainer (text) {
+    options.resultsContainer.innerHTML += text
+  }
+
+  function registerInput () {
+    options.searchInput.addEventListener('input', function (e) {
+      if (isWhitelistedKey(e.which)) {
+        emptyResultsContainer()
+        debounce(function () { search(e.target.value) }, options.debounceTime)
+      }
+    })
+  }
+
+  function search (query) {
+    if (isValidQuery(query)) {
+      emptyResultsContainer()
+      render(_$Repository_4.search(query), query)
+    }
+  }
+
+  function render (results, query) {
+    const len = results.length
+    if (len === 0) {
+      return appendToResultsContainer(options.noResultsText)
+    }
+    for (let i = 0; i < len; i++) {
+      results[i].query = query
+      appendToResultsContainer(_$Templater_7.compile(results[i]))
+    }
+  }
+
+  function isValidQuery (query) {
+    return query && query.length > 0
+  }
+
+  function isWhitelistedKey (key) {
+    return [13, 16, 20, 37, 38, 39, 40, 91].indexOf(key) === -1
+  }
+
+  function throwError (message) {
+    throw new Error('SimpleJekyllSearch --- ' + message)
+  }
+})(window)
+
+}());
diff --git a/search.json b/search.json
new file mode 100644
index 000000000..b1937a0e3
--- /dev/null
+++ b/search.json
@@ -0,0 +1,29 @@
+---
+---
+[
+  {% for post in site.posts %}
+    {
+
+      "title"    : "{{ post.title | escape }}",
+      "url"      : "{{ site.baseurl }}{{ post.url }}",
+      "category" : "{{ post.category }}",
+      "tags"     : "{{ post.tags | join: ', ' }}",
+      "date"     : "{{ post.date }}",
+      "content"  : "{{ post.content | strip_html | strip_newlines | remove_chars | escape }}"
+
+    } {% unless forloop.last %},{% endunless %}
+  {% endfor %}
+  ,
+  {% for page in site.pages %}
+   {
+     {% if page.title != nil %}
+        "title"    : "{{ page.title | escape }}",
+        "category" : "{{ page.category }}",
+        "tags"     : "{{ page.tags | join: ', ' }}",
+        "url"      : "{{ site.baseurl }}{{ page.url }}",
+        "date"     : "{{ page.date }}",
+        "content"  : "{{ page.content | strip_html | strip_newlines | remove_chars | escape }}"
+     {% endif %}
+   } {% unless forloop.last %},{% endunless %}
+  {% endfor %}
+]
diff --git a/search.md b/search.md
new file mode 100644
index 000000000..09f3b6923
--- /dev/null
+++ b/search.md
@@ -0,0 +1,46 @@
+---
+title: Search in hifis.net
+title_image: default
+layout: default
+excerpt:
+    Search in hifis.net
+---
+
+# Search in hifis.net
+{:.text-success}
+
+Type in your keywords.
+Maximum number of results shown is 20.
+
+<form role="search">
+<div id="form-group">
+    <input type="search" id="search-input" placeholder="Search..." class="form-control">
+    <ul id="results-container"></ul>
+</div>
+
+<script src="{{ site.baseurl }}/js/simple-jekyll-search.js"></script>
+
+<script>
+    window.simpleJekyllSearch = new SimpleJekyllSearch({
+    searchInput: document.getElementById('search-input'),
+    resultsContainer: document.getElementById('results-container'),
+    json: '{{ site.baseurl }}/search.json',
+    searchResultTemplate: '<li><a href="{url}?query={query}" title="{desc}">{title}</a></li>',
+    noResultsText: 'No results found',
+    limit: 20,
+    fuzzy: false,
+    exclude: ['Welcome']
+    })
+</script>
+</form>
+
+For this search functionality, [Simple Jekyll Search](https://github.com/christian-fei/Simple-Jekyll-Search) is used, which is released under [MIT license](https://github.com/christian-fei/Simple-Jekyll-Search/blob/master/LICENSE.md).
+
+## Missing anything?
+
+If you have suggestions, questions, or queries, please don't hesitate to write us.
+
+<a href="{% link contact.md %}" 
+                            class="btn btn-outline-secondary"
+                            title="HIFIS Helpdesk"> 
+                            Contact us! <i class="fas fa-envelope"></i></a>
-- 
GitLab