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 @@