Skip to content

Commit 594f097

Browse files
kckernclaude
andcommitted
test(web): pin highlighting for queries with leading or trailing punctuation
PHP's preg_split gives an empty piece for 'faith charity!', and its per-word pattern then wraps every word and inserts empty spans. Node drops the empty piece and wraps only words that start like a query word; the comment at the prefix set records this as an intended difference, and a test pins Node's output. Also notes that an empty piece matches any one word, so the MAX_GAP cap limits a gap to 8 words. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
1 parent 48f83e1 commit 594f097

2 files changed

Lines changed: 12 additions & 0 deletions

File tree

‎apps/web/src/services/SearchService.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,8 @@ function joinPieces(pieces) {
7777
// so a long row of them makes the regex try every way of splitting each run it crosses (a query of
7878
// 'the', 1000 spaces and 'lord' took 40 s on one verse). Rows of more than MAX_GAP empty pieces
7979
// (more than MAX_GAP + 1 spaces or dots in a row, which no realistic query has) are cut to MAX_GAP.
80+
// An empty piece also matches any one word between its separators, so a row of k empty pieces
81+
// spans up to k words; the cap limits such a gap to MAX_GAP (8) words.
8082
const MAX_GAP = 8;
8183
function prefixPieces(pieces) {
8284
const out = [];
@@ -93,6 +95,10 @@ export function makeHighlighter(query) {
9395
const phrase = `\\b${joinPieces(pieces.map(escapeRegex))}[a-z]*`;
9496
const prefixes = `\\b${prefixPieces(pieces).map(p => `${escapeRegex(p.slice(0, 3))}[a-z]*`).join('\\W+')}[a-z]*`;
9597
const stages = [phrase, prefixes].map(src => ({ test: new RegExp(src, 'i'), all: new RegExp(src, 'gi') }));
98+
// The per-word step's three-letter prefixes. PHP's preg_split('/[^a-z]+/i') gives an empty
99+
// piece for a leading or trailing non-letter ('faith charity!'), and its pattern /\b[a-z]*/i
100+
// wraps every word and puts an empty span at each word boundary (a live bug). Intended
101+
// difference: Node drops empty pieces and wraps only words that start like a query word.
96102
const words = new Set(String(query).split(/[^a-z]+/i).filter(Boolean).map(q => q.slice(0, 3).toLowerCase()));
97103
const marked = w => {
98104
const l = w.toLowerCase();

‎apps/web/test/unit/searchService.test.js‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,12 @@ describe('helpers', () => {
212212
expect(out).not.toMatch(/<span[^>]*>[^<]*<span/);
213213
expect(Date.now() - start).toBeLessThan(1000);
214214
});
215+
test('leading or trailing punctuation highlights only the query words (intended difference from live)', () => {
216+
// Live's empty preg_split piece wraps every word and adds empty spans; Node does not
217+
const out = "<span class='searchterm'>charity</span> and <span class='searchterm'>faith</span>";
218+
expect(highlight('charity and faith', 'faith charity!')).toBe(out);
219+
expect(highlight('charity and faith', '!faith charity')).toBe(out);
220+
});
215221
test('highlight treats repeated separators as live does', () => {
216222
expect(highlight('faith, hope', 'faith hope')).toBe("<span class='searchterm'>faith, hope</span>");
217223
expect(highlight('faith hope', 'faith hope')).toBe("<span class='searchterm'>faith</span> <span class='searchterm'>hope</span>");

0 commit comments

Comments
 (0)