diff --git a/README.md b/README.md
index 28da376..50b0e3b 100644
--- a/README.md
+++ b/README.md
@@ -97,6 +97,7 @@ Here is a table for the available options, usage questions, troubleshooting & gu
| `success` | Function | No | A function called once the data has been loaded. |
| `debounceTime` | Number | No | Limit how many times the search function can be executed over the given time window. If no `debounceTime` (milliseconds) is provided a search will be triggered on each keystroke. |
| `searchResultTemplate` | String | No | The template of a single rendered search result. (match liquid value eg: `'
{title}'` |
+| `templateMiddleware` | Function | No | A function that processes template placeholders and can include highlighting functionality. The function receives (prop, value, template, query) parameters. |
## Middleware
@@ -139,3 +140,71 @@ SimpleJekyllSearch({
},
})
```
+
+### templateMiddleware with Highlighting (Function) [optional]
+
+The `templateMiddleware` function now supports highlighting functionality. It processes template placeholders and can add HTML highlighting to search results.
+
+The function receives four parameters:
+- `prop`: The property name being processed (e.g., 'title', 'content', 'desc')
+- `value`: The value of the property
+- `template`: The full template string
+- `query`: The search query (optional)
+
+Example with built-in highlight template middleware:
+
+```js
+import { createHighlightTemplateMiddleware } from 'simple-jekyll-search';
+
+SimpleJekyllSearch({
+ // ...other config
+ templateMiddleware: createHighlightTemplateMiddleware({
+ highlightClass: 'my-highlight', // Custom CSS class
+ contextBefore: 30, // Characters before match
+ contextAfter: 30, // Characters after match
+ maxLength: 200, // Maximum total length
+ ellipsis: '...' // Text to show when truncated
+ }),
+})
+```
+
+You can also create a custom template middleware with highlighting:
+
+```js
+SimpleJekyllSearch({
+ // ...other config
+ templateMiddleware: function(prop, value, template, query) {
+ // Only highlight content and desc fields
+ if ((prop === 'content' || prop === 'desc') && query && typeof value === 'string') {
+ return value.replace(
+ new RegExp(`(${query})`, 'gi'),
+ '$1'
+ );
+ }
+ return undefined; // Use default value for other properties
+ },
+})
+```
+
+The template middleware should return a string to replace the placeholder, or `undefined` to use the default value.
+
+#### Search Strategy Compatibility
+
+The highlight template middleware works with all search strategies:
+
+- **Literal Search**: Highlights exact word matches
+- **Wildcard Search**: Highlights exact matches (supports regex patterns)
+- **Fuzzy Search**: Highlights both exact matches and fuzzy matches (e.g., "test" matches "tst", "testing")
+
+The middleware intelligently handles different search types:
+1. First tries exact word matching (for literal and wildcard searches)
+2. Falls back to fuzzy matching if no exact matches found
+3. Prefers exact matches over fuzzy matches when both are available
+
+Example with fuzzy search:
+```js
+// Searching for "test" will highlight:
+// - "test" (exact match)
+// - "testing" (fuzzy match - highlights "test" part)
+// - "tst" (fuzzy match - highlights "tst")
+```
diff --git a/cypress.config.ts b/cypress.config.ts
index 7b0d806..e2ae597 100644
--- a/cypress.config.ts
+++ b/cypress.config.ts
@@ -11,11 +11,13 @@ export default defineConfig({
on('before:run', (details) => {
console.log('🚀 Starting Cypress tests:', details.specs?.length || 0, 'spec(s) to run');
console.log(`Running on: ${details.browser?.name} ${details.browser?.version}`);
+ console.log('📝 Make sure Jekyll server is running at http://localhost:4000/Simple-Jekyll-Search/');
+ console.log('💡 Run: cd docs && bundle exec jekyll serve --baseurl /Simple-Jekyll-Search');
return Promise.resolve();
});
on('after:run', (_results) => {
- console.log('✅ Cypress test run completed!');
+ console.log('✅ Cypress test run completed!');
return Promise.resolve();
});
},
diff --git a/dest/simple-jekyll-search.js b/dest/simple-jekyll-search.js
index ce5759d..679fe02 100644
--- a/dest/simple-jekyll-search.js
+++ b/dest/simple-jekyll-search.js
@@ -80,11 +80,8 @@
const cost = a[i - 1] === b[j - 1] ? 0 : 1;
distanceMatrix[i][j] = Math.min(
distanceMatrix[i - 1][j] + 1,
- // Removing a character from one string
distanceMatrix[i][j - 1] + 1,
- // Adding a character to one string to make it closer to the other string.
distanceMatrix[i - 1][j - 1] + cost
- // Replacing one character in a string with another
);
}
}
@@ -157,7 +154,7 @@
success: function() {
},
searchResultTemplate: '{title}',
- templateMiddleware: (_prop, _value, _template) => void 0,
+ templateMiddleware: (_prop, _value, _template, _query) => void 0,
sortMiddleware: NoSort,
noResultsText: "No results found",
limit: 10,
@@ -282,9 +279,9 @@
options.middleware = _options.middleware;
}
}
- function compile(data) {
+ function compile(data, query) {
return options.template.replace(options.pattern, function(match, prop) {
- const value = options.middleware(prop, data[prop], options.template);
+ const value = options.middleware(prop, data[prop], options.template, query);
if (typeof value !== "undefined") {
return value;
}
@@ -357,7 +354,7 @@
results.forEach((result) => {
result.query = query;
const div = document.createElement("div");
- div.innerHTML = compile(result);
+ div.innerHTML = compile(result, query);
fragment.appendChild(div);
});
this.options.resultsContainer.appendChild(fragment);
@@ -392,13 +389,249 @@
return rv;
}
};
+ function createHighlightMiddleware(options2 = {}) {
+ const {
+ highlightClass = "sjs-highlight",
+ contextBefore = 50,
+ contextAfter = 50,
+ maxLength = 250,
+ ellipsis = "..."
+ } = options2;
+ return function(result, query) {
+ if (!query || !result) {
+ return result;
+ }
+ const highlightedResult = { ...result };
+ const textFields = ["title", "desc", "content", "excerpt"];
+ for (const field of textFields) {
+ if (highlightedResult[field] && typeof highlightedResult[field] === "string") {
+ const highlighted = highlightText(
+ highlightedResult[field],
+ query,
+ { highlightClass, contextBefore, contextAfter, maxLength, ellipsis }
+ );
+ if (highlighted.highlightedText !== highlightedResult[field]) {
+ highlightedResult[field] = highlighted.highlightedText;
+ }
+ }
+ }
+ return highlightedResult;
+ };
+ }
+ function highlightText(text, query, options2 = {}) {
+ const {
+ highlightClass = "sjs-highlight",
+ contextBefore = 50,
+ contextAfter = 50,
+ maxLength = 250,
+ ellipsis = "..."
+ } = options2;
+ if (!text || !query) {
+ return { highlightedText: text, matchCount: 0 };
+ }
+ const originalText = text;
+ const searchTerms = query.trim().toLowerCase().split(/\s+/).filter((term) => term.length > 0);
+ if (searchTerms.length === 0) {
+ return { highlightedText: text, matchCount: 0 };
+ }
+ const matches = [];
+ for (const term of searchTerms) {
+ let index = 0;
+ while (index < text.length) {
+ const found = text.toLowerCase().indexOf(term, index);
+ if (found === -1) break;
+ matches.push({
+ start: found,
+ end: found + term.length,
+ term: text.substring(found, found + term.length)
+ });
+ index = found + 1;
+ }
+ }
+ if (matches.length === 0) {
+ return { highlightedText: text, matchCount: 0 };
+ }
+ matches.sort((a, b) => a.start - b.start);
+ const mergedMatches = [];
+ for (const match of matches) {
+ if (mergedMatches.length === 0 || mergedMatches[mergedMatches.length - 1].end < match.start) {
+ mergedMatches.push(match);
+ } else {
+ const lastMatch = mergedMatches[mergedMatches.length - 1];
+ lastMatch.end = Math.max(lastMatch.end, match.end);
+ lastMatch.term = text.substring(lastMatch.start, lastMatch.end);
+ }
+ }
+ if (text.length <= maxLength) {
+ let highlightedText2 = text;
+ for (let i = mergedMatches.length - 1; i >= 0; i--) {
+ const match = mergedMatches[i];
+ const before = highlightedText2.substring(0, match.start);
+ const after = highlightedText2.substring(match.end);
+ const matchText = highlightedText2.substring(match.start, match.end);
+ highlightedText2 = before + `${matchText}` + after;
+ }
+ return { highlightedText: highlightedText2, matchCount: mergedMatches.length };
+ }
+ let highlightedText = "";
+ let totalLength = 0;
+ let lastEnd = 0;
+ for (let i = 0; i < mergedMatches.length; i++) {
+ const match = mergedMatches[i];
+ const contextStart = Math.max(lastEnd, match.start - contextBefore);
+ const contextEnd = Math.min(text.length, match.end + contextAfter);
+ if (contextStart > lastEnd && lastEnd > 0) {
+ highlightedText += ellipsis;
+ totalLength += ellipsis.length;
+ }
+ if (contextStart < match.start) {
+ const beforeText = text.substring(contextStart, match.start);
+ highlightedText += beforeText;
+ totalLength += beforeText.length;
+ }
+ const matchText = text.substring(match.start, match.end);
+ highlightedText += `${matchText}`;
+ totalLength += matchText.length;
+ if (match.end < contextEnd) {
+ const afterText = text.substring(match.end, contextEnd);
+ highlightedText += afterText;
+ totalLength += afterText.length;
+ }
+ lastEnd = contextEnd;
+ if (totalLength >= maxLength) {
+ if (contextEnd < text.length) {
+ highlightedText += ellipsis;
+ }
+ break;
+ }
+ }
+ return {
+ highlightedText: highlightedText || originalText,
+ matchCount: mergedMatches.length
+ };
+ }
+ const defaultHighlightMiddleware = createHighlightMiddleware();
+ function findFuzzyMatches(text, pattern) {
+ const matches = [];
+ const lowerText = text.toLowerCase();
+ const lowerPattern = pattern.toLowerCase().trim();
+ if (lowerPattern.length === 0) return matches;
+ const patternWords = lowerPattern.split(/\s+/);
+ for (const word of patternWords) {
+ if (word.length === 0) continue;
+ const wordMatches = findFuzzyWordMatches(lowerText, word);
+ matches.push(...wordMatches);
+ }
+ return matches;
+ }
+ function findFuzzyWordMatches(text, word) {
+ const matches = [];
+ const exactRegex = new RegExp(`\\b${word.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\b`, "gi");
+ let match;
+ while ((match = exactRegex.exec(text)) !== null) {
+ matches.push({
+ start: match.index,
+ end: match.index + match[0].length,
+ text: match[0]
+ });
+ }
+ if (matches.length === 0) {
+ for (let i = 0; i < text.length; i++) {
+ const fuzzyMatch = findFuzzySequenceMatch(text, word, i);
+ if (fuzzyMatch) {
+ matches.push(fuzzyMatch);
+ i = fuzzyMatch.end - 1;
+ }
+ }
+ }
+ return matches;
+ }
+ function findFuzzySequenceMatch(text, pattern, startPos) {
+ let textIndex = startPos;
+ let patternIndex = 0;
+ let matchStart = -1;
+ let maxGap = 3;
+ const matchedPositions = [];
+ while (textIndex < text.length && patternIndex < pattern.length) {
+ if (text[textIndex] === pattern[patternIndex]) {
+ if (matchStart === -1) {
+ matchStart = textIndex;
+ }
+ matchedPositions.push(textIndex);
+ patternIndex++;
+ } else if (matchStart !== -1) {
+ if (textIndex - matchStart > maxGap * pattern.length) {
+ return null;
+ }
+ }
+ textIndex++;
+ }
+ if (patternIndex === pattern.length && matchStart !== -1 && matchedPositions.length > 0) {
+ const actualStart = matchedPositions[0];
+ const actualEnd = matchedPositions[matchedPositions.length - 1] + 1;
+ return {
+ start: actualStart,
+ end: actualEnd,
+ text: text.substring(actualStart, actualEnd)
+ };
+ }
+ return null;
+ }
+ function createHighlightTemplateMiddleware(options2 = {}) {
+ const highlightOptions = {
+ highlightClass: "sjs-highlight",
+ ...options2
+ };
+ return function(prop, value, _template, query) {
+ if ((prop === "content" || prop === "desc") && query && typeof value === "string") {
+ let highlightedText = value;
+ let hasMatches = false;
+ const searchTerms = query.trim().split(/\s+/).filter((term) => term.length > 0);
+ for (const term of searchTerms) {
+ const regex = new RegExp(`\\b(${term.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")})\\b`, "gi");
+ if (regex.test(highlightedText)) {
+ highlightedText = highlightedText.replace(regex, `$1`);
+ hasMatches = true;
+ }
+ }
+ if (!hasMatches) {
+ const fuzzyMatches = findFuzzyMatches(value, query);
+ if (fuzzyMatches.length > 0) {
+ fuzzyMatches.sort((a, b) => b.start - a.start);
+ for (const match of fuzzyMatches) {
+ const before = highlightedText.substring(0, match.start);
+ const after = highlightedText.substring(match.end);
+ const matchText = highlightedText.substring(match.start, match.end);
+ highlightedText = before + `${matchText}` + after;
+ }
+ hasMatches = true;
+ }
+ }
+ if (hasMatches) {
+ return highlightedText;
+ }
+ }
+ return void 0;
+ };
+ }
+ const defaultHighlightTemplateMiddleware = createHighlightTemplateMiddleware();
function SimpleJekyllSearch(options2) {
const instance = new SimpleJekyllSearch$1();
return instance.init(options2);
}
if (typeof window !== "undefined") {
window.SimpleJekyllSearch = SimpleJekyllSearch;
+ window.SimpleJekyllSearch.createHighlightMiddleware = createHighlightMiddleware;
+ window.SimpleJekyllSearch.createHighlightTemplateMiddleware = createHighlightTemplateMiddleware;
+ window.SimpleJekyllSearch.highlightText = highlightText;
+ window.SimpleJekyllSearch.defaultHighlightMiddleware = defaultHighlightMiddleware;
+ window.SimpleJekyllSearch.defaultHighlightTemplateMiddleware = defaultHighlightTemplateMiddleware;
}
+ exports2.createHighlightMiddleware = createHighlightMiddleware;
+ exports2.createHighlightTemplateMiddleware = createHighlightTemplateMiddleware;
exports2.default = SimpleJekyllSearch;
+ exports2.defaultHighlightMiddleware = defaultHighlightMiddleware;
+ exports2.defaultHighlightTemplateMiddleware = defaultHighlightTemplateMiddleware;
+ exports2.highlightText = highlightText;
Object.defineProperties(exports2, { __esModule: { value: true }, [Symbol.toStringTag]: { value: "Module" } });
});
diff --git a/dest/simple-jekyll-search.min.js b/dest/simple-jekyll-search.min.js
index 58f1487..62446e1 100644
--- a/dest/simple-jekyll-search.min.js
+++ b/dest/simple-jekyll-search.min.js
@@ -4,4 +4,4 @@
* Copyright 2025-2025, Sylhare
* Licensed under the MIT License.
*/
-(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?factory(exports):typeof define==="function"&&define.amd?define(["exports"],factory):(global=typeof globalThis!=="undefined"?globalThis:global||self,factory(global.SimpleJekyllSearch={}))})(this,(function(exports2){"use strict";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 instanceof Error?err:new Error(String(err)),null)}}}}function getXHR(){return window.XMLHttpRequest?new window.XMLHttpRequest:new window.ActiveXObject("Microsoft.XMLHTTP")}class OptionsValidator{constructor(params){if(!this.validateParams(params)){throw new Error("-- OptionsValidator: required options missing")}this.requiredOptions=params.required}getRequiredOptions(){return this.requiredOptions}validate(parameters){const errors=[];this.requiredOptions.forEach((requiredOptionName=>{if(typeof parameters[requiredOptionName]==="undefined"){errors.push(requiredOptionName)}}));return errors}validateParams(params){if(!params){return false}return typeof params.required!=="undefined"&&Array.isArray(params.required)}}function fuzzySearch(text,pattern){pattern=pattern.trimEnd();if(pattern.length===0)return true;pattern=pattern.toLowerCase();text=text.toLowerCase();let remainingText=text,currentIndex=-1;for(const char of pattern){const nextIndex=remainingText.indexOf(char);if(nextIndex===-1||currentIndex!==-1&&remainingText.slice(0,nextIndex).split(" ").length-1>2){return false}currentIndex=nextIndex;remainingText=remainingText.slice(nextIndex+1)}return true}function literalSearch(text,criteria){text=text.trim().toLowerCase();const pattern=criteria.endsWith(" ")?[criteria.toLowerCase()]:criteria.trim().toLowerCase().split(" ");return pattern.filter((word=>text.indexOf(word)>=0)).length===pattern.length}function levenshtein(a,b){const lenA=a.length;const lenB=b.length;const distanceMatrix=Array.from({length:lenA+1},(()=>Array(lenB+1).fill(0)));for(let i=0;i<=lenA;i++)distanceMatrix[i][0]=i;for(let j=0;j<=lenB;j++)distanceMatrix[0][j]=j;for(let i=1;i<=lenA;i++){for(let j=1;j<=lenB;j++){const cost=a[i-1]===b[j-1]?0:1;distanceMatrix[i][j]=Math.min(distanceMatrix[i-1][j]+1,distanceMatrix[i][j-1]+1,distanceMatrix[i-1][j-1]+cost)}}return distanceMatrix[lenA][lenB]}function levenshteinSearch(text,pattern){const distance=levenshtein(pattern,text);const similarity=1-distance/Math.max(pattern.length,text.length);return similarity>=.3}function wildcardSearch(text,pattern){const regexPattern=pattern.replace(/\*/g,".*");const regex=new RegExp(`^${regexPattern}$`,"i");if(regex.test(text))return true;return levenshteinSearch(text,pattern)}class SearchStrategy{constructor(matchFunction){this.matchFunction=matchFunction}matches(text,criteria){if(text===null||text.trim()===""||!criteria){return false}return this.matchFunction(text,criteria)}}const LiteralSearchStrategy=new SearchStrategy(literalSearch);const FuzzySearchStrategy=new SearchStrategy(((text,criteria)=>fuzzySearch(text,criteria)||literalSearch(text,criteria)));const WildcardSearchStrategy=new SearchStrategy(((text,criteria)=>wildcardSearch(text,criteria)||literalSearch(text,criteria)));function merge(target,source){return{...target,...source}}function isJSON(json){try{return!!(json instanceof Object&&JSON.parse(JSON.stringify(json)))}catch(_err){return false}}function NoSort(){return 0}function isObject(obj){return Boolean(obj)&&Object.prototype.toString.call(obj)==="[object Object]"}function clone(input){if(input===null||typeof input!=="object"){return input}if(Array.isArray(input)){return input.map((item=>clone(item)))}const output={};for(const key in input){if(Object.prototype.hasOwnProperty.call(input,key)){output[key]=clone(input[key])}}return output}const DEFAULT_OPTIONS={searchInput:null,resultsContainer:null,json:[],success:function(){},searchResultTemplate:'{title}',templateMiddleware:(_prop,_value,_template)=>void 0,sortMiddleware:NoSort,noResultsText:"No results found",limit:10,fuzzy:false,strategy:"literal",debounceTime:null,exclude:[],onSearch:()=>{}};const REQUIRED_OPTIONS=["searchInput","resultsContainer","json"];const WHITELISTED_KEYS=new Set(["Enter","Shift","CapsLock","ArrowLeft","ArrowUp","ArrowRight","ArrowDown","Meta"]);class Repository{constructor(initialOptions={}){this.data=[];this.setOptions(initialOptions)}put(input){if(isObject(input)){return this.addObject(input)}if(Array.isArray(input)){return this.addArray(input)}return void 0}clear(){this.data.length=0;return this.data}search(criteria){if(!criteria){return[]}return clone(this.findMatches(this.data,criteria).sort(this.options.sortMiddleware))}setOptions(newOptions){this.options={fuzzy:(newOptions==null?void 0:newOptions.fuzzy)||DEFAULT_OPTIONS.fuzzy,limit:(newOptions==null?void 0:newOptions.limit)||DEFAULT_OPTIONS.limit,searchStrategy:this.searchStrategy((newOptions==null?void 0:newOptions.strategy)||newOptions.fuzzy&&"fuzzy"||DEFAULT_OPTIONS.strategy),sortMiddleware:(newOptions==null?void 0:newOptions.sortMiddleware)||DEFAULT_OPTIONS.sortMiddleware,exclude:(newOptions==null?void 0:newOptions.exclude)||DEFAULT_OPTIONS.exclude,strategy:(newOptions==null?void 0:newOptions.strategy)||DEFAULT_OPTIONS.strategy}}addObject(obj){this.data.push(obj);return this.data}addArray(arr){const added=[];this.clear();for(const item of arr){if(isObject(item)){added.push(this.addObject(item)[0])}}return added}findMatches(data,criteria){const matches=[];for(let i=0;i{if(err){this.throwError(`Failed to load JSON from ${url}: ${err.message}`)}this.initWithJSON(json)}))}registerInput(){this.options.searchInput.addEventListener("input",(e=>{const inputEvent=e;if(!WHITELISTED_KEYS.has(inputEvent.key)){this.emptyResultsContainer();this.debounce((()=>{this.search(e.target.value)}),this.options.debounceTime??null)}}))}search(query){var _a,_b;if((query==null?void 0:query.trim().length)>0){this.emptyResultsContainer();const results=this.repository.search(query);this.render(results,query);(_b=(_a=this.options).onSearch)==null?void 0:_b.call(_a)}}render(results,query){if(results.length===0){this.options.resultsContainer.insertAdjacentHTML("beforeend",this.options.noResultsText);return}const fragment=document.createDocumentFragment();results.forEach((result=>{result.query=query;const div=document.createElement("div");div.innerHTML=compile(result);fragment.appendChild(div)}));this.options.resultsContainer.appendChild(fragment)}init(_options){var _a;const errors=this.optionsValidator.validate(_options);if(errors.length>0){this.throwError(`Missing required options: ${REQUIRED_OPTIONS.join(", ")}`)}this.options=merge(this.options,_options);setOptions({template:this.options.searchResultTemplate,middleware:this.options.templateMiddleware});this.repository.setOptions({fuzzy:this.options.fuzzy,limit:this.options.limit,sortMiddleware:this.options.sortMiddleware,strategy:this.options.strategy,exclude:this.options.exclude});if(isJSON(this.options.json)){this.initWithJSON(this.options.json)}else{this.initWithURL(this.options.json)}const rv={search:this.search.bind(this)};(_a=this.options.success)==null?void 0:_a.call(rv);return rv}};function SimpleJekyllSearch(options2){const instance=new SimpleJekyllSearch$1;return instance.init(options2)}if(typeof window!=="undefined"){window.SimpleJekyllSearch=SimpleJekyllSearch}exports2.default=SimpleJekyllSearch;Object.defineProperties(exports2,{__esModule:{value:true},[Symbol.toStringTag]:{value:"Module"}})}));
\ No newline at end of file
+(function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?factory(exports):typeof define==="function"&&define.amd?define(["exports"],factory):(global=typeof globalThis!=="undefined"?globalThis:global||self,factory(global.SimpleJekyllSearch={}))})(this,(function(exports2){"use strict";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 instanceof Error?err:new Error(String(err)),null)}}}}function getXHR(){return window.XMLHttpRequest?new window.XMLHttpRequest:new window.ActiveXObject("Microsoft.XMLHTTP")}class OptionsValidator{constructor(params){if(!this.validateParams(params)){throw new Error("-- OptionsValidator: required options missing")}this.requiredOptions=params.required}getRequiredOptions(){return this.requiredOptions}validate(parameters){const errors=[];this.requiredOptions.forEach((requiredOptionName=>{if(typeof parameters[requiredOptionName]==="undefined"){errors.push(requiredOptionName)}}));return errors}validateParams(params){if(!params){return false}return typeof params.required!=="undefined"&&Array.isArray(params.required)}}function fuzzySearch(text,pattern){pattern=pattern.trimEnd();if(pattern.length===0)return true;pattern=pattern.toLowerCase();text=text.toLowerCase();let remainingText=text,currentIndex=-1;for(const char of pattern){const nextIndex=remainingText.indexOf(char);if(nextIndex===-1||currentIndex!==-1&&remainingText.slice(0,nextIndex).split(" ").length-1>2){return false}currentIndex=nextIndex;remainingText=remainingText.slice(nextIndex+1)}return true}function literalSearch(text,criteria){text=text.trim().toLowerCase();const pattern=criteria.endsWith(" ")?[criteria.toLowerCase()]:criteria.trim().toLowerCase().split(" ");return pattern.filter((word=>text.indexOf(word)>=0)).length===pattern.length}function levenshtein(a,b){const lenA=a.length;const lenB=b.length;const distanceMatrix=Array.from({length:lenA+1},(()=>Array(lenB+1).fill(0)));for(let i=0;i<=lenA;i++)distanceMatrix[i][0]=i;for(let j=0;j<=lenB;j++)distanceMatrix[0][j]=j;for(let i=1;i<=lenA;i++){for(let j=1;j<=lenB;j++){const cost=a[i-1]===b[j-1]?0:1;distanceMatrix[i][j]=Math.min(distanceMatrix[i-1][j]+1,distanceMatrix[i][j-1]+1,distanceMatrix[i-1][j-1]+cost)}}return distanceMatrix[lenA][lenB]}function levenshteinSearch(text,pattern){const distance=levenshtein(pattern,text);const similarity=1-distance/Math.max(pattern.length,text.length);return similarity>=.3}function wildcardSearch(text,pattern){const regexPattern=pattern.replace(/\*/g,".*");const regex=new RegExp(`^${regexPattern}$`,"i");if(regex.test(text))return true;return levenshteinSearch(text,pattern)}class SearchStrategy{constructor(matchFunction){this.matchFunction=matchFunction}matches(text,criteria){if(text===null||text.trim()===""||!criteria){return false}return this.matchFunction(text,criteria)}}const LiteralSearchStrategy=new SearchStrategy(literalSearch);const FuzzySearchStrategy=new SearchStrategy(((text,criteria)=>fuzzySearch(text,criteria)||literalSearch(text,criteria)));const WildcardSearchStrategy=new SearchStrategy(((text,criteria)=>wildcardSearch(text,criteria)||literalSearch(text,criteria)));function merge(target,source){return{...target,...source}}function isJSON(json){try{return!!(json instanceof Object&&JSON.parse(JSON.stringify(json)))}catch(_err){return false}}function NoSort(){return 0}function isObject(obj){return Boolean(obj)&&Object.prototype.toString.call(obj)==="[object Object]"}function clone(input){if(input===null||typeof input!=="object"){return input}if(Array.isArray(input)){return input.map((item=>clone(item)))}const output={};for(const key in input){if(Object.prototype.hasOwnProperty.call(input,key)){output[key]=clone(input[key])}}return output}const DEFAULT_OPTIONS={searchInput:null,resultsContainer:null,json:[],success:function(){},searchResultTemplate:'{title}',templateMiddleware:(_prop,_value,_template,_query)=>void 0,sortMiddleware:NoSort,noResultsText:"No results found",limit:10,fuzzy:false,strategy:"literal",debounceTime:null,exclude:[],onSearch:()=>{}};const REQUIRED_OPTIONS=["searchInput","resultsContainer","json"];const WHITELISTED_KEYS=new Set(["Enter","Shift","CapsLock","ArrowLeft","ArrowUp","ArrowRight","ArrowDown","Meta"]);class Repository{constructor(initialOptions={}){this.data=[];this.setOptions(initialOptions)}put(input){if(isObject(input)){return this.addObject(input)}if(Array.isArray(input)){return this.addArray(input)}return void 0}clear(){this.data.length=0;return this.data}search(criteria){if(!criteria){return[]}return clone(this.findMatches(this.data,criteria).sort(this.options.sortMiddleware))}setOptions(newOptions){this.options={fuzzy:(newOptions==null?void 0:newOptions.fuzzy)||DEFAULT_OPTIONS.fuzzy,limit:(newOptions==null?void 0:newOptions.limit)||DEFAULT_OPTIONS.limit,searchStrategy:this.searchStrategy((newOptions==null?void 0:newOptions.strategy)||newOptions.fuzzy&&"fuzzy"||DEFAULT_OPTIONS.strategy),sortMiddleware:(newOptions==null?void 0:newOptions.sortMiddleware)||DEFAULT_OPTIONS.sortMiddleware,exclude:(newOptions==null?void 0:newOptions.exclude)||DEFAULT_OPTIONS.exclude,strategy:(newOptions==null?void 0:newOptions.strategy)||DEFAULT_OPTIONS.strategy}}addObject(obj){this.data.push(obj);return this.data}addArray(arr){const added=[];this.clear();for(const item of arr){if(isObject(item)){added.push(this.addObject(item)[0])}}return added}findMatches(data,criteria){const matches=[];for(let i=0;i{if(err){this.throwError(`Failed to load JSON from ${url}: ${err.message}`)}this.initWithJSON(json)}))}registerInput(){this.options.searchInput.addEventListener("input",(e=>{const inputEvent=e;if(!WHITELISTED_KEYS.has(inputEvent.key)){this.emptyResultsContainer();this.debounce((()=>{this.search(e.target.value)}),this.options.debounceTime??null)}}))}search(query){var _a,_b;if((query==null?void 0:query.trim().length)>0){this.emptyResultsContainer();const results=this.repository.search(query);this.render(results,query);(_b=(_a=this.options).onSearch)==null?void 0:_b.call(_a)}}render(results,query){if(results.length===0){this.options.resultsContainer.insertAdjacentHTML("beforeend",this.options.noResultsText);return}const fragment=document.createDocumentFragment();results.forEach((result=>{result.query=query;const div=document.createElement("div");div.innerHTML=compile(result,query);fragment.appendChild(div)}));this.options.resultsContainer.appendChild(fragment)}init(_options){var _a;const errors=this.optionsValidator.validate(_options);if(errors.length>0){this.throwError(`Missing required options: ${REQUIRED_OPTIONS.join(", ")}`)}this.options=merge(this.options,_options);setOptions({template:this.options.searchResultTemplate,middleware:this.options.templateMiddleware});this.repository.setOptions({fuzzy:this.options.fuzzy,limit:this.options.limit,sortMiddleware:this.options.sortMiddleware,strategy:this.options.strategy,exclude:this.options.exclude});if(isJSON(this.options.json)){this.initWithJSON(this.options.json)}else{this.initWithURL(this.options.json)}const rv={search:this.search.bind(this)};(_a=this.options.success)==null?void 0:_a.call(rv);return rv}};function createHighlightMiddleware(options2={}){const{highlightClass:highlightClass="sjs-highlight",contextBefore:contextBefore=50,contextAfter:contextAfter=50,maxLength:maxLength=250,ellipsis:ellipsis="..."}=options2;return function(result,query){if(!query||!result){return result}const highlightedResult={...result};const textFields=["title","desc","content","excerpt"];for(const field of textFields){if(highlightedResult[field]&&typeof highlightedResult[field]==="string"){const highlighted=highlightText(highlightedResult[field],query,{highlightClass:highlightClass,contextBefore:contextBefore,contextAfter:contextAfter,maxLength:maxLength,ellipsis:ellipsis});if(highlighted.highlightedText!==highlightedResult[field]){highlightedResult[field]=highlighted.highlightedText}}}return highlightedResult}}function highlightText(text,query,options2={}){const{highlightClass:highlightClass="sjs-highlight",contextBefore:contextBefore=50,contextAfter:contextAfter=50,maxLength:maxLength=250,ellipsis:ellipsis="..."}=options2;if(!text||!query){return{highlightedText:text,matchCount:0}}const originalText=text;const searchTerms=query.trim().toLowerCase().split(/\s+/).filter((term=>term.length>0));if(searchTerms.length===0){return{highlightedText:text,matchCount:0}}const matches=[];for(const term of searchTerms){let index=0;while(indexa.start-b.start));const mergedMatches=[];for(const match of matches){if(mergedMatches.length===0||mergedMatches[mergedMatches.length-1].end=0;i--){const match=mergedMatches[i];const before=highlightedText2.substring(0,match.start);const after=highlightedText2.substring(match.end);const matchText=highlightedText2.substring(match.start,match.end);highlightedText2=before+`${matchText}`+after}return{highlightedText:highlightedText2,matchCount:mergedMatches.length}}let highlightedText="";let totalLength=0;let lastEnd=0;for(let i=0;ilastEnd&&lastEnd>0){highlightedText+=ellipsis;totalLength+=ellipsis.length}if(contextStart${matchText}`;totalLength+=matchText.length;if(match.end=maxLength){if(contextEndmaxGap*pattern.length){return null}}textIndex++}if(patternIndex===pattern.length&&matchStart!==-1&&matchedPositions.length>0){const actualStart=matchedPositions[0];const actualEnd=matchedPositions[matchedPositions.length-1]+1;return{start:actualStart,end:actualEnd,text:text.substring(actualStart,actualEnd)}}return null}function createHighlightTemplateMiddleware(options2={}){const highlightOptions={highlightClass:"sjs-highlight",...options2};return function(prop,value,_template,query){if((prop==="content"||prop==="desc")&&query&&typeof value==="string"){let highlightedText=value;let hasMatches=false;const searchTerms=query.trim().split(/\s+/).filter((term=>term.length>0));for(const term of searchTerms){const regex=new RegExp(`\\b(${term.replace(/[.*+?^${}()|[\]\\]/g,"\\$&")})\\b`,"gi");if(regex.test(highlightedText)){highlightedText=highlightedText.replace(regex,`$1`);hasMatches=true}}if(!hasMatches){const fuzzyMatches=findFuzzyMatches(value,query);if(fuzzyMatches.length>0){fuzzyMatches.sort(((a,b)=>b.start-a.start));for(const match of fuzzyMatches){const before=highlightedText.substring(0,match.start);const after=highlightedText.substring(match.end);const matchText=highlightedText.substring(match.start,match.end);highlightedText=before+`${matchText}`+after}hasMatches=true}}if(hasMatches){return highlightedText}}return void 0}}const defaultHighlightTemplateMiddleware=createHighlightTemplateMiddleware();function SimpleJekyllSearch(options2){const instance=new SimpleJekyllSearch$1;return instance.init(options2)}if(typeof window!=="undefined"){window.SimpleJekyllSearch=SimpleJekyllSearch;window.SimpleJekyllSearch.createHighlightMiddleware=createHighlightMiddleware;window.SimpleJekyllSearch.createHighlightTemplateMiddleware=createHighlightTemplateMiddleware;window.SimpleJekyllSearch.highlightText=highlightText;window.SimpleJekyllSearch.defaultHighlightMiddleware=defaultHighlightMiddleware;window.SimpleJekyllSearch.defaultHighlightTemplateMiddleware=defaultHighlightTemplateMiddleware}exports2.createHighlightMiddleware=createHighlightMiddleware;exports2.createHighlightTemplateMiddleware=createHighlightTemplateMiddleware;exports2.default=SimpleJekyllSearch;exports2.defaultHighlightMiddleware=defaultHighlightMiddleware;exports2.defaultHighlightTemplateMiddleware=defaultHighlightTemplateMiddleware;exports2.highlightText=highlightText;Object.defineProperties(exports2,{__esModule:{value:true},[Symbol.toStringTag]:{value:"Module"}})}));
\ No newline at end of file
diff --git a/docs/_config.yml b/docs/_config.yml
index b338841..753b154 100644
--- a/docs/_config.yml
+++ b/docs/_config.yml
@@ -8,4 +8,14 @@ url: "https://sylhare.github.io"
# Build settings
markdown: kramdown
sass:
- style: compressed
\ No newline at end of file
+ style: compressed
+
+# Exclude from processing
+exclude:
+ - Gemfile
+ - Gemfile.lock
+ - node_modules
+ - vendor/bundle/
+ - vendor/cache/
+ - vendor/gems/
+ - vendor/ruby/
\ No newline at end of file
diff --git a/docs/_includes/search.html b/docs/_includes/search.html
index a9e5150..c5c2b65 100644
--- a/docs/_includes/search.html
+++ b/docs/_includes/search.html
@@ -7,11 +7,20 @@