Skip to content

Commit bfe89ad

Browse files
xr843claude
andauthored
Split search_index.js into pure JS logic and Liquid data template (#637)
* Split search_index.js into pure JS logic and Liquid data template Extract pure JavaScript functions (getPositions, resultMatched, addMatchHighlights, getBlurbForResult, categoryName, displaySearchResult) into a separate search_functions.js file. This separates testable logic from the Jekyll/Liquid template, enabling direct unit testing without needing to parse the Liquid template or run a Jekyll build first. search_index.js now imports search_functions.js via importScripts and retains only the Liquid-generated data (store), lunr index building, and the web worker message handler. No functional changes — same behavior, just file reorganization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Move displaySearchResults and onmessage handler to search_functions.js Per review feedback, also extract the message handling code and displaySearchResults so search_index.js is as JS-lean as possible. search_index.js now contains only the Liquid data template, lunr pipeline setup, and index building. The moved functions reference globals (store, idx) that are defined at runtime by search_index.js — this works because importScripts runs synchronously and the onmessage handler is only called after the worker finishes initialization. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Remove header comment and make message handler a pure function - Remove file header comments per review feedback - Convert self.onmessage into a pure handleSearchMessage function that returns the message object; search_index.js wires up self Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> * Add semicolon after OBU_STEMMER to fix ASI issue The removed BMAX/RMAX lines previously provided newlines between the OBU_STEMMER closing brace and the Liquid {%- tags. Without them, the Liquid whitespace stripping concatenates '}' and 'var store' on the same line, preventing automatic semicolon insertion. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
1 parent 74b1187 commit bfe89ad

2 files changed

Lines changed: 227 additions & 225 deletions

File tree

‎assets/js/search_functions.js‎

Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
// Parameters
2+
var BMAX = 250; // Max blurb size in characters
3+
var RMAX = 100; // Max number of results to display
4+
5+
function getPositions(result, field) {
6+
var positions = [];
7+
var md = result.matchData.metadata;
8+
for (var searchTerm in md) {
9+
var fieldResults = md[searchTerm][field];
10+
if (!fieldResults) continue;
11+
for (var p in fieldResults.position) sortedInsert(positions, fieldResults.position[p][0]);
12+
}
13+
return positions;
14+
}
15+
16+
function resultMatched(result, fromfield) {
17+
let md = result.matchData.metadata;
18+
for (var st in md) {
19+
if (!md[st][fromfield]) continue;
20+
for (var mi in md[st][fromfield]['position']) return true;
21+
}
22+
return false;
23+
}
24+
25+
function addMatchHighlights(result, blurb, fromfield, startindex, endindex) {
26+
var startindex = startindex || 0;
27+
var endindex = endindex || blurb.length;
28+
var ranges = new Ranges();
29+
let md = result.matchData.metadata;
30+
var i = 0; var j = 0;
31+
for (var searchTerm in md) {
32+
if (!md[searchTerm][fromfield]) continue;
33+
for (var mi in md[searchTerm][fromfield].position) {
34+
let m = md[searchTerm][fromfield].position[mi];
35+
i = m[0]; j = i+m[1];
36+
if (i < endindex && j > startindex) {
37+
ranges.add([(startindex>i?startindex:i)-startindex, j-startindex]);
38+
}
39+
}
40+
}
41+
i = ranges.array.length - 1;
42+
var ret = blurb;
43+
while (i >= 0) {
44+
ret = ret.substring(0,ranges.array[i][0]) +
45+
'<strong>' + ret.substring(ranges.array[i][0],ranges.array[i][1]) +
46+
'</strong>' + ret.substring(ranges.array[i][1], ret.length);
47+
i--;
48+
}
49+
return ret;
50+
}
51+
52+
function getBlurbForResult(result, item, positions) {
53+
var titleMatch = false;
54+
let md = result.matchData.metadata;
55+
for (var searchTerm in md) {
56+
if (md[searchTerm]['title']) {
57+
titleMatch = true;
58+
break;
59+
}
60+
}
61+
if ((titleMatch || positions.length == 0) && item.description){
62+
var ret = item.description;
63+
if (item.description.length > BMAX)
64+
ret = item.description.substring(0, BMAX) + "...";
65+
return addMatchHighlights(result, ret, 'description');
66+
}
67+
// Calculate the best section of the content to blurb
68+
var best_i = -1;
69+
var best_n = 0;
70+
for (var i in positions) {
71+
var n = 1;
72+
var j = i+1;
73+
while (j < positions.length) {
74+
if (positions[j] > positions[i] + BMAX) break;
75+
n++; j++;
76+
}
77+
if (n > best_n) {
78+
best_n = n;
79+
best_i = i;
80+
}
81+
}
82+
i = positions[best_i];
83+
j = positions[best_i + best_n - 1];
84+
var m = (i+j)>>1;
85+
var startindex = m - (BMAX>>1);
86+
var pre = true;
87+
if (startindex <= 0){ startindex = 0; pre = false; }
88+
var endindex = startindex + BMAX;
89+
var ret = item.content.substring(startindex,endindex);
90+
ret = addMatchHighlights(result, ret, 'content', startindex, endindex);
91+
if (endindex >= item.content.length) return (pre?'...':'') + ret;
92+
return (pre?'...':'') + ret + "...";
93+
}
94+
95+
function categoryName(c) {
96+
switch(c) {
97+
case 'av': return '<i class="fas fa-volume-up"></i> Recording';
98+
case 'articles': return '<i class="far fa-newspaper"></i> Article';
99+
case 'booklets': return '<i class="fas fa-book-open"></i> Book';
100+
case 'monographs': return '<i class="fas fa-book"></i> Book';
101+
case 'papers': return '<i class="far fa-file-powerpoint"></i> Paper';
102+
case 'essays': return '<i class="far fa-file-word"></i> Essay';
103+
case 'canon': return '<i class="fas fa-dharmachakra"></i> Canonical Work';
104+
case 'reference': return '<i class="fas fa-atlas"></i> Reference Work';
105+
case 'excerpts': return '<i class="fas fa-book-reader"></i> Excerpt';
106+
}
107+
return '<i class="far fa-file"></i> Library Item';
108+
}
109+
110+
function displaySearchResult(result, item) {
111+
var positions = getPositions(result, 'content');
112+
var blurb = getBlurbForResult(result, item, positions);
113+
var type = null;
114+
switch (item.type) {
115+
case 'courses': type = '<i class="fas fa-chalkboard"></i> Course'; break;
116+
case 'content': type = categoryName(item.category); break;
117+
case 'posts': type = '<i class="fas fa-rss"></i> Blog Post'; break;
118+
case 'journals': type = '<i class="fas fa-newspaper"></i> Journal'; break;
119+
case 'authors': type = '<i class="far fa-address-book"></i> Author'; break;
120+
case 'publishers': type = '<i class="far fa-building"></i> Publisher'; break;
121+
case 'tags': type = '<i class="fas fa-tag"></i> Bibliography'; break;
122+
case 'series': type = '<i class="fas fa-list-ol"></i> Series'; break;
123+
}
124+
var ret = '<li><h3><a href="' + item.url + '">' + addMatchHighlights(result, item.title, 'title') + '</a></h3>';
125+
if (type) ret += '<span class="Counter">' + type + '</span>';
126+
var lc = 0;
127+
for (var i in item.authors) {
128+
ret += '<span class="Label ml-1">' +
129+
addMatchHighlights(result, item.authors[i], 'author', lc, lc + item.authors[i].length) +
130+
'</span>';
131+
lc += 2 + item.authors[i].length;
132+
}
133+
if (resultMatched(result, 'translator')) ret += '<span class="Label ml-1">Translator: ' +
134+
addMatchHighlights(result, item.translator, 'translator') +
135+
'</span>';
136+
if (resultMatched(result, 'tag')) {
137+
lc = 0;
138+
for (var i in item.tags) {
139+
if (!item.tags[i]) continue;
140+
let t = '<span class="Label ml-1 text-capitalize"><i class="fas fa-tag"></i> ' +
141+
addMatchHighlights(result, item.tags[i].replace('-',' '), 'tag', lc, lc + item.tags[i].length) + '</span>';
142+
if (t.includes('<strong>')) ret += t;
143+
lc += 1 + item.tags[i].length;
144+
}
145+
}
146+
if (resultMatched(result, 'format')) {
147+
lc = 0;
148+
for (var i in item.formats) {
149+
if (!item.formats[i]) continue;
150+
let t = '<span class="Label ml-1"><i class="fas fa-file-arrow-down"></i> ' +
151+
addMatchHighlights(result, item.formats[i], 'format', lc, lc + item.formats[i].length) + '</span>';
152+
if (t.includes('<strong>')) ret += t;
153+
lc += 2 + item.formats[i].length;
154+
}
155+
}
156+
return ret + '<p>' + blurb + '</p></li>';
157+
}
158+
159+
function displaySearchResults(results) {
160+
if (results.length) {
161+
var ret = '';
162+
for (var i in results) {
163+
if (i >= RMAX) break;
164+
ret += displaySearchResult(results[i], store[results[i].ref]);
165+
}
166+
return ret;
167+
} else {
168+
return '<li>No results found</li>';
169+
}
170+
}
171+
172+
function handleSearchMessage(data, searchFn) {
173+
var results = [];
174+
var warning = "";
175+
var words = data.q.trim().split(" ");
176+
for (var i = 0; i < words.length; i++) {
177+
const s = words[i].trim();
178+
if (!s.startsWith("+") && !s.startsWith("-") && s.length > 1 && lunr.stopWordFilter(s)) {
179+
words[i] = "+" + s;
180+
} else {
181+
words[i] = s;
182+
}
183+
}
184+
var query = words.join(' ').trim();
185+
try {
186+
results = searchFn(query);
187+
if (!results.length){
188+
warning = "<li><strong>No results</strong> found matching all of your terms. Results found matching <em>any</em> term:</li>";
189+
results = searchFn(data.q.trim());
190+
}
191+
} catch (err) {
192+
if (err.message.indexOf("unrecognised field") >= 0 && query.indexOf(":") >= 0) {
193+
results = searchFn(data.q.replaceAll(":"," "));
194+
} else { throw err; }
195+
}
196+
if (!results.length){
197+
words = data.q.trim().split(" ");
198+
if (words.find(function(w){ return w.length <= 2; }) == undefined) {
199+
results = searchFn(words.join("~1 ") + "~1");
200+
if (results.length)
201+
warning = "<li><strong>No results</strong> found for your query. Perhaps you meant:</li>";
202+
else
203+
warning = "";
204+
} else {
205+
warning = "";
206+
}
207+
}
208+
if (data.filterquery && data.filterquery !== "") {
209+
var filteredResults = searchFn(data.filterquery);
210+
results = results.filter(function(result) {
211+
return filteredResults.some(function(filteredResult) {
212+
return filteredResult.ref === result.ref;
213+
});
214+
});
215+
}
216+
return {
217+
"warninghtml": warning,
218+
"html": displaySearchResults(results),
219+
"count": results ? results.length : 0,
220+
"q": data.q,
221+
"filterquery": data.filterquery,
222+
"qt": data.qt
223+
};
224+
}

0 commit comments

Comments
 (0)