Skip to content

Commit cb64fde

Browse files
committed
fix: restore crawler search highlights
Read crawler snippet highlight markup from _snippetResult.content.value instead of the plain content field. Rebuild API result titles from crawler hierarchy values so Array.map-style matches can still be highlighted in the custom DocSearch hit renderer.
1 parent e7c911e commit cb64fde

3 files changed

Lines changed: 205 additions & 21 deletions

File tree

__tests__/Search_.test.res

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ open Vitest
44
// Helper
55
// ---------------------------------------------------------------------------
66

7+
let highlightedValue = (value: string): DocSearch.highlightedValue => {value: value}
8+
79
let makeHit = (~type_: DocSearch.contentType, ~url: string): DocSearch.docSearchHit => {
810
objectID: "test",
911
content: Nullable.null,
@@ -21,8 +23,8 @@ let makeHit = (~type_: DocSearch.contentType, ~url: string): DocSearch.docSearch
2123
lvl6: Nullable.null,
2224
},
2325
deprecated: None,
24-
_highlightResult: Obj.magic(Dict.make()),
25-
_snippetResult: Obj.magic(Dict.make()),
26+
_highlightResult: {hierarchy: Nullable.null},
27+
_snippetResult: {content: Nullable.null},
2628
}
2729

2830
// ---------------------------------------------------------------------------
@@ -160,6 +162,64 @@ test(
160162
},
161163
)
162164

165+
test("getHighlightedTitle rebuilds crawler API titles and preserves marked prefixes", async () => {
166+
let hit = {
167+
...makeHit(
168+
~type_=Content,
169+
~url="https://rescript-lang.org/docs/manual/api/stdlib/array/#value-mapWithIndex",
170+
),
171+
hierarchy: {
172+
lvl0: Nullable.make("Array"),
173+
lvl1: Nullable.make("mapWithIndex"),
174+
lvl2: Nullable.null,
175+
lvl3: Nullable.null,
176+
lvl4: Nullable.null,
177+
lvl5: Nullable.null,
178+
lvl6: Nullable.null,
179+
},
180+
_snippetResult: {
181+
content: Nullable.make(highlightedValue("See <mark>Array.map</mark> on MDN.")),
182+
},
183+
}
184+
185+
expect(Search.getHighlightedTitle(hit))->toBe("<mark>Array.map</mark>WithIndex")
186+
})
187+
188+
test(
189+
"getHighlightedTitle prefers real hierarchy highlights when Algolia returns them",
190+
async () => {
191+
let highlightedHierarchy: DocSearch.highlightedHierarchy = {
192+
lvl0: Nullable.null,
193+
lvl1: Nullable.null,
194+
lvl2: Nullable.make(highlightedValue("<mark>Section</mark> title")),
195+
lvl3: Nullable.null,
196+
lvl4: Nullable.null,
197+
lvl5: Nullable.null,
198+
lvl6: Nullable.null,
199+
}
200+
let hit = {
201+
...makeHit(~type_=Lvl2, ~url="https://rescript-lang.org/docs/manual/page#section"),
202+
_highlightResult: {hierarchy: Nullable.make(highlightedHierarchy)},
203+
}
204+
205+
expect(Search.getHighlightedTitle(hit))->toBe("<mark>Section</mark> title")
206+
},
207+
)
208+
209+
test("getContentHtml prefers crawler snippet markup over plain content", async () => {
210+
let hit = {
211+
...makeHit(~type_=Content, ~url="https://rescript-lang.org/docs/manual/api/stdlib/array/"),
212+
content: Nullable.make("map(array, fn) returns a new array."),
213+
_snippetResult: {
214+
content: Nullable.make(highlightedValue("map(array, fn) returns a new <mark>array</mark>.")),
215+
},
216+
}
217+
218+
expect(Search.getContentHtml(hit))->toEqual(
219+
Some("map(array, fn) returns a new <mark>array</mark>."),
220+
)
221+
})
222+
163223
// ---------------------------------------------------------------------------
164224
// isChildHit
165225
// ---------------------------------------------------------------------------

src/bindings/DocSearch.res

Lines changed: 22 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,26 @@ type hierarchy = {
1919
lvl6: Nullable.t<string>,
2020
}
2121

22+
type highlightedValue = {value: string}
23+
24+
type highlightedHierarchy = {
25+
lvl0: Nullable.t<highlightedValue>,
26+
lvl1: Nullable.t<highlightedValue>,
27+
lvl2: Nullable.t<highlightedValue>,
28+
lvl3: Nullable.t<highlightedValue>,
29+
lvl4: Nullable.t<highlightedValue>,
30+
lvl5: Nullable.t<highlightedValue>,
31+
lvl6: Nullable.t<highlightedValue>,
32+
}
33+
34+
type highlightResult = {
35+
hierarchy: Nullable.t<highlightedHierarchy>,
36+
}
37+
38+
type snippetResult = {
39+
content: Nullable.t<highlightedValue>,
40+
}
41+
2242
type docSearchHit = {
2343
objectID: string,
2444
content: Nullable.t<string>,
@@ -30,8 +50,8 @@ type docSearchHit = {
3050
// Additional field for deprecation information
3151
deprecated: option<string>,
3252
// NOTE: docsearch need these two fields to highlight results
33-
_highlightResult: {.},
34-
_snippetResult: {.},
53+
_highlightResult: highlightResult,
54+
_snippetResult: snippetResult,
3555
}
3656
type transformItems = array<docSearchHit>
3757

src/components/Search.res

Lines changed: 121 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -36,22 +36,6 @@ let navigator = (~siteUrl: string): DocSearch.navigator => {
3636
},
3737
}
3838

39-
let getHighlightedTitle: DocSearch.docSearchHit => string = %raw(`
40-
function(hit) {
41-
var type = hit.type;
42-
var h = hit._highlightResult && hit._highlightResult.hierarchy;
43-
var raw = hit.hierarchy;
44-
try {
45-
if (type && type !== 'lvl1' && type !== 'lvl0') {
46-
var lvl = h && h[type] && h[type].value;
47-
if (lvl) return lvl;
48-
}
49-
if (h && h.lvl1 && h.lvl1.value) return h.lvl1.value;
50-
} catch(e) {}
51-
return (raw && raw.lvl1) || '';
52-
}
53-
`)
54-
5539
let getSubtitle: DocSearch.docSearchHit => option<string> = %raw(`
5640
function(hit) {
5741
var type = hit.type;
@@ -63,6 +47,120 @@ let getSubtitle: DocSearch.docSearchHit => option<string> = %raw(`
6347
}
6448
`)
6549

50+
let highlightedValue = (value: Nullable.t<DocSearch.highlightedValue>): option<string> =>
51+
value->Nullable.toOption->Option.map(value => value.value)
52+
53+
let highlightedValueWithMarkup = (value: Nullable.t<DocSearch.highlightedValue>): option<string> =>
54+
switch highlightedValue(value) {
55+
| Some(value) if value->String.includes("<mark>") => Some(value)
56+
| _ => None
57+
}
58+
59+
let highlightedHierarchyValue = (
60+
hierarchy: DocSearch.highlightedHierarchy,
61+
type_: DocSearch.contentType,
62+
): option<string> =>
63+
switch type_ {
64+
| Lvl0 => hierarchy.lvl0->highlightedValue
65+
| Lvl1 => hierarchy.lvl1->highlightedValue
66+
| Lvl2 => hierarchy.lvl2->highlightedValue
67+
| Lvl3 => hierarchy.lvl3->highlightedValue
68+
| Lvl4 => hierarchy.lvl4->highlightedValue
69+
| Lvl5 => hierarchy.lvl5->highlightedValue
70+
| Lvl6 => hierarchy.lvl6->highlightedValue
71+
| Content => None
72+
}
73+
74+
let highlightedHierarchyValueWithMarkup = (
75+
hierarchy: DocSearch.highlightedHierarchy,
76+
type_: DocSearch.contentType,
77+
): option<string> =>
78+
switch type_ {
79+
| Lvl0 => hierarchy.lvl0->highlightedValueWithMarkup
80+
| Lvl1 => hierarchy.lvl1->highlightedValueWithMarkup
81+
| Lvl2 => hierarchy.lvl2->highlightedValueWithMarkup
82+
| Lvl3 => hierarchy.lvl3->highlightedValueWithMarkup
83+
| Lvl4 => hierarchy.lvl4->highlightedValueWithMarkup
84+
| Lvl5 => hierarchy.lvl5->highlightedValueWithMarkup
85+
| Lvl6 => hierarchy.lvl6->highlightedValueWithMarkup
86+
| Content => None
87+
}
88+
89+
let firstMarkedText = (html: string): option<string> => {
90+
switch RegExp.exec(/<mark>([^<]+)<\/mark>/, html) {
91+
| Some(result) =>
92+
let matches = RegExp.Result.matches(result)
93+
switch matches[0] {
94+
| Some(Some(markedText)) => Some(markedText)
95+
| _ => None
96+
}
97+
| None => None
98+
}
99+
}
100+
101+
let markTitlePrefix = (title: string, markedText: string): string => {
102+
let markedLength = String.length(markedText)
103+
if (
104+
markedLength > 0 && title->String.toLowerCase->String.startsWith(markedText->String.toLowerCase)
105+
) {
106+
let prefix = String.slice(title, ~start=0, ~end=markedLength)
107+
let suffix = String.slice(title, ~start=markedLength)
108+
`<mark>${prefix}</mark>${suffix}`
109+
} else {
110+
title
111+
}
112+
}
113+
114+
let getSnippetContent = (hit: DocSearch.docSearchHit): option<string> =>
115+
hit._snippetResult.content->highlightedValue
116+
117+
let getApiTitle = (hit: DocSearch.docSearchHit): option<string> => {
118+
if hit.url->String.includes("/docs/manual/api/") {
119+
switch (hit.hierarchy.lvl0->Nullable.toOption, hit.hierarchy.lvl1->Nullable.toOption) {
120+
| (Some(moduleName), Some(valueName)) if moduleName !== "" && valueName !== "" =>
121+
let title = `${moduleName}.${valueName}`
122+
switch hit->getSnippetContent->Option.flatMap(firstMarkedText) {
123+
| Some(markedText) => Some(markTitlePrefix(title, markedText))
124+
| None => Some(title)
125+
}
126+
| _ => None
127+
}
128+
} else {
129+
None
130+
}
131+
}
132+
133+
let getHighlightedTitle = (hit: DocSearch.docSearchHit): string => {
134+
let highlightedHierarchy = hit._highlightResult.hierarchy->Nullable.toOption
135+
let highlightedTitleWithMarkup = highlightedHierarchy->Option.flatMap(hierarchy =>
136+
switch hit.type_ {
137+
| Lvl0 | Lvl1 => None
138+
| _ => highlightedHierarchyValueWithMarkup(hierarchy, hit.type_)
139+
}
140+
)
141+
142+
switch highlightedTitleWithMarkup {
143+
| Some(title) => title
144+
| None =>
145+
switch highlightedHierarchy->Option.flatMap(hierarchy =>
146+
hierarchy.lvl1->highlightedValueWithMarkup
147+
) {
148+
| Some(title) => title
149+
| None =>
150+
switch getApiTitle(hit) {
151+
| Some(title) => title
152+
| None =>
153+
switch highlightedHierarchy->Option.flatMap(hierarchy =>
154+
highlightedHierarchyValue(hierarchy, hit.type_)
155+
) {
156+
| Some(title) => title
157+
| None => hit.hierarchy.lvl1->Nullable.toOption->Option.getOr("")
158+
}
159+
}
160+
}
161+
}
162+
}
163+
66164
let markdownToHtml = (text: string): string =>
67165
text
68166
// Strip stray backslashes from MDX processing
@@ -90,10 +188,16 @@ let isChildHit = (hit: DocSearch.docSearchHit) =>
90188
| Lvl0 | Lvl1 => hit.url->String.includes("#")
91189
}
92190

191+
let getContentHtml = (hit: DocSearch.docSearchHit): option<string> =>
192+
switch getSnippetContent(hit) {
193+
| Some(content) => Some(content->markdownToHtml)
194+
| None => hit.content->Nullable.toOption->Option.map(markdownToHtml)
195+
}
196+
93197
let hitComponent = ({hit, children: _}: DocSearch.hitComponent): React.element => {
94198
let titleHtml = getHighlightedTitle(hit)
95199
let subtitle = getSubtitle(hit)
96-
let contentHtml = hit.content->Nullable.toOption->Option.map(markdownToHtml)
200+
let contentHtml = getContentHtml(hit)
97201
let isChild = isChildHit(hit)
98202

99203
<a href={hit.url}>

0 commit comments

Comments
 (0)