diff --git a/_data/navigation.yml b/_data/navigation.yml index e50f747d4515a2f970103326bd937ae18cb9f0fe..a422db1df9d58a018ce28655fcf56af4c1921d6c 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 0000000000000000000000000000000000000000..29d2869fb85979eece1b910a2f6f040becfb5bb1 --- /dev/null +++ b/_plugins/simple_search_filter.rb @@ -0,0 +1,19 @@ +module Jekyll + module CharFilter + def remove_chars(input) + input.gsub! '\\','\' + 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 0000000000000000000000000000000000000000..cfbb66b0a4cfe55f5c84a12d361dd25d4cacd723 --- /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 0000000000000000000000000000000000000000..b1937a0e334e7c3dfeaad2bc4bfff51b2dd3cd7f --- /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 0000000000000000000000000000000000000000..09f3b6923c90c46b31f1d79c148f3c8c818359a1 --- /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>