Skip to content

Commit cf4a465

Browse files
committed
feat: make pagefind the new default search engine
1 parent a3f4219 commit cf4a465

File tree

5 files changed

+346
-12
lines changed

5 files changed

+346
-12
lines changed

assets/default/pagefind.css

Lines changed: 152 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,152 @@
1+
#multi-page-nav .search {
2+
margin-left: auto;
3+
font-size: 16px;
4+
}
5+
6+
#multi-page-nav .search .search-keybinding {
7+
float: right;
8+
width: 0;
9+
transform: translateX(-1em);
10+
color: #999;
11+
}
12+
13+
#multi-page-nav .search:focus-within .search-keybinding {
14+
display: none;
15+
}
16+
17+
#multi-page-nav .search #search-input {
18+
border: 1px solid #666;
19+
border-radius: 3.2px;
20+
color: #999;
21+
background-color: unset;
22+
height: 28px;
23+
font-family: inherit;
24+
width: 20em;
25+
font-size: 14px;
26+
padding: 0 8px;
27+
}
28+
29+
#multi-page-nav .search #search-input::placeholder {
30+
color: #999;
31+
opacity: 1;
32+
}
33+
34+
#multi-page-nav .search:focus-within #search-input {
35+
background-color: #fff;
36+
outline: none;
37+
box-shadow: none;
38+
color: #000;
39+
}
40+
41+
.theme--documenter-dark #multi-page-nav .search:focus-within #search-input {
42+
background-color: #202227;
43+
color: #eee;
44+
}
45+
46+
#multi-page-nav .search:focus-within .suggestions {
47+
display: block;
48+
}
49+
50+
#multi-page-nav .hidden {
51+
display: none !important;
52+
}
53+
54+
#multi-page-nav .suggestions {
55+
margin: -5px 1.5rem 0 0;
56+
display: none;
57+
background: #fff;
58+
min-width: 20em;
59+
max-width: 50vw;
60+
position: absolute;
61+
border: 1px solid #cfd4db;
62+
border-radius: 6px;
63+
padding: .4rem;
64+
list-style-type: none;
65+
z-index: 10;
66+
right: 0;
67+
max-height: max(50vh, 250px);
68+
overflow-y: auto;
69+
}
70+
71+
.theme--documenter-dark #multi-page-nav .suggestions {
72+
background: #2e3138;
73+
border: 1px solid #5e6d6f;
74+
}
75+
76+
#multi-page-nav .suggestions mark {
77+
background-color: inherit;
78+
color: #000;
79+
}
80+
.theme--documenter-dark #multi-page-nav .suggestions mark {
81+
color: #fff;
82+
}
83+
84+
#multi-page-nav .suggestions .suggestion {
85+
line-height: 1.3;
86+
border-radius: 4px;
87+
88+
overflow: hidden;
89+
text-overflow: ellipsis;
90+
word-break: break-all;
91+
}
92+
#multi-page-nav .suggestions .sub-suggestions .suggestion {
93+
margin-left: 1rem;
94+
}
95+
96+
#multi-page-nav .suggestions .suggestion-header {
97+
padding: .4rem .6rem;
98+
}
99+
#multi-page-nav .suggestions .suggestion-header:focus-visible {
100+
outline: none;
101+
}
102+
.theme--documenter-dark #multi-page-nav .suggestions .suggestion-header:hover,
103+
.theme--documenter-dark #multi-page-nav .suggestions .suggestion-header:focus {
104+
background-color: #202227;
105+
}
106+
#multi-page-nav .suggestions .suggestion-header:hover,
107+
#multi-page-nav .suggestions .suggestion-header:focus {
108+
background-color: #eee;
109+
}
110+
111+
#multi-page-nav .suggestions .suggestion .suggestion-excerpt {
112+
font-size: small;
113+
color: #666;
114+
}
115+
.theme--documenter-dark #multi-page-nav .suggestions .suggestion .suggestion-excerpt {
116+
color: #aaa;
117+
}
118+
119+
#multi-page-nav .suggestion .suggestion-title {
120+
font-size: 1.2rem;
121+
font-weight: bold;
122+
}
123+
124+
#multi-page-nav .suggestion .sub-suggestions .suggestion-title {
125+
font-size: 1rem;
126+
}
127+
128+
#multi-page-nav .suggestion a {
129+
display: block;
130+
overflow: hidden;
131+
text-overflow: ellipsis;
132+
color: black;
133+
}
134+
135+
.theme--documenter-dark #multi-page-nav .suggestion a {
136+
color: white;
137+
}
138+
139+
#multi-page-nav .suggestions .suggestion .page-title {
140+
font-weight: bold;
141+
}
142+
143+
@media screen and (max-width: 1055px) {
144+
#multi-page-nav .search #search-input {
145+
width: 100%;
146+
}
147+
#multi-page-nav .suggestions {
148+
max-width: 100vw;
149+
width: calc(100% - 3.5rem);
150+
margin: 10px 2.5em 4.5em 0;
151+
}
152+
}
Lines changed: 150 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,150 @@
1+
// custom search widget
2+
(async function() {
3+
const MAX_RESULTS = 20
4+
let FOCUSABLE_ELEMENTS = []
5+
let FOCUSED_ELEMENT_INDEX = 0
6+
7+
const pagefind = await import("/pagefind/pagefind.js");
8+
9+
function initialize() {
10+
pagefind.init()
11+
registerSearchListener()
12+
13+
document.body.addEventListener('keydown', ev => {
14+
if (document.activeElement === document.body && (ev.key === '/' || ev.key === 's')) {
15+
document.getElementById('search-input').focus()
16+
ev.preventDefault()
17+
}
18+
})
19+
}
20+
21+
function registerSearchListener() {
22+
const input = document.getElementById('search-input')
23+
const suggestions = document.getElementById('search-result-container')
24+
25+
async function runSearch() {
26+
const query = input.value
27+
28+
const search = await pagefind.debouncedSearch(query, {}, 300);
29+
30+
if (search) {
31+
buildResults(search.results)
32+
}
33+
}
34+
35+
input.addEventListener('keyup', ev => {
36+
runSearch()
37+
})
38+
39+
input.addEventListener('keydown', ev => {
40+
if (ev.key === 'ArrowDown') {
41+
FOCUSED_ELEMENT_INDEX = 0
42+
FOCUSABLE_ELEMENTS[FOCUSED_ELEMENT_INDEX].focus()
43+
ev.preventDefault()
44+
return
45+
} else if (ev.key === 'ArrowUp') {
46+
FOCUSED_ELEMENT_INDEX = FOCUSABLE_ELEMENTS.length - 1
47+
FOCUSABLE_ELEMENTS[FOCUSED_ELEMENT_INDEX].focus()
48+
ev.preventDefault()
49+
return
50+
}
51+
})
52+
53+
suggestions.addEventListener('keydown', ev => {
54+
if (ev.key === 'ArrowDown') {
55+
FOCUSED_ELEMENT_INDEX += 1
56+
if (FOCUSED_ELEMENT_INDEX < FOCUSABLE_ELEMENTS.length) {
57+
FOCUSABLE_ELEMENTS[FOCUSED_ELEMENT_INDEX].focus()
58+
} else {
59+
FOCUSED_ELEMENT_INDEX = -1
60+
input.focus()
61+
}
62+
ev.preventDefault()
63+
} else if (ev.key === 'ArrowUp') {
64+
FOCUSED_ELEMENT_INDEX -= 1
65+
if (FOCUSED_ELEMENT_INDEX >= 0) {
66+
FOCUSABLE_ELEMENTS[FOCUSED_ELEMENT_INDEX].focus()
67+
} else {
68+
FOCUSED_ELEMENT_INDEX = -1
69+
input.focus()
70+
}
71+
ev.preventDefault()
72+
}
73+
})
74+
75+
input.addEventListener('focus', ev => {
76+
runSearch()
77+
})
78+
}
79+
80+
function renderResult(result) {
81+
const entry = document.createElement('li')
82+
entry.classList.add('suggestion')
83+
84+
const linkContainer = document.createElement('a')
85+
linkContainer.classList.add('suggestion-header')
86+
linkContainer.setAttribute('href', result.url)
87+
88+
const page = document.createElement('p')
89+
page.classList.add('suggestion-title')
90+
91+
const pageTitle = document.createElement('span')
92+
pageTitle.innerText = result.title ?? result.meta.title
93+
94+
page.appendChild(pageTitle)
95+
96+
const excerpt = document.createElement('p')
97+
excerpt.classList.add('suggestion-excerpt')
98+
excerpt.innerHTML = result.excerpt
99+
100+
linkContainer.appendChild(page)
101+
linkContainer.appendChild(excerpt)
102+
103+
entry.appendChild(linkContainer)
104+
105+
return entry
106+
}
107+
108+
async function buildResults(results) {
109+
const suggestions = document.getElementById('search-result-container')
110+
111+
const children = await Promise.all(results.slice(0, MAX_RESULTS - 1).map(async (r, i) => {
112+
const data = await r.data()
113+
114+
const entry = renderResult(data)
115+
116+
if (data.sub_results.length > 0) {
117+
const subResults = document.createElement('ol')
118+
subResults.classList.add('sub-suggestions')
119+
120+
data.sub_results.forEach(subresult => {
121+
const entry = renderResult(subresult)
122+
subResults.appendChild(entry)
123+
})
124+
entry.appendChild(subResults)
125+
}
126+
127+
return entry
128+
}))
129+
130+
if (results.length > 0) {
131+
suggestions.classList.remove('hidden')
132+
} else {
133+
suggestions.classList.add('hidden')
134+
}
135+
136+
137+
suggestions.replaceChildren(
138+
...children
139+
)
140+
141+
FOCUSED_ELEMENT_INDEX = -1
142+
FOCUSABLE_ELEMENTS = [...suggestions.querySelectorAll('a')]
143+
}
144+
145+
if (document.readyState === 'loading') {
146+
document.addEventListener('DOMContentLoaded', initialize)
147+
} else {
148+
initialize()
149+
};
150+
})()

src/MultiDocumenter.jl

Lines changed: 6 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -11,17 +11,17 @@ include("documentertools/canonical_urls.jl")
1111
end
1212

1313
"""
14-
SearchConfig(index_versions = ["stable"], engine = MultiDocumenter.FlexSearch, lowfi = false)
14+
SearchConfig(index_versions = ["stable"], engine = MultiDocumenter.PageFind, lowfi = false)
1515
1616
`index_versions` is a vector of relative paths used for generating the search index. Only
1717
the first matching path is considered.
18-
`engine` may be `MultiDocumenter.FlexSearch`, `MultiDocumenter.Stork`, or a module that conforms
19-
to the expected API (which is currently undocumented).
18+
`engine` may be `MultiDocumenter.PageFind`, `MultiDocumenter.FlexSearch`, `MultiDocumenter.Stork`,
19+
or a module that conforms to the expected API (which is currently undocumented).
2020
`lowfi = true` will try to minimize search index size. Only relevant for flexsearch.
2121
"""
2222
Base.@kwdef mutable struct SearchConfig
2323
index_versions = ["stable", "dev"]
24-
engine = FlexSearch
24+
engine = PageFind
2525
lowfi = false
2626
end
2727

@@ -131,12 +131,13 @@ function walk_outputs(f, root, docs::Vector, dirs::Vector{String})
131131
end
132132

133133
include("renderers.jl")
134+
include("search/pagefind.jl")
134135
include("search/flexsearch.jl")
135136
include("search/stork.jl")
136137
include("canonical.jl")
137138
include("sitemap.jl")
138139

139-
const DEFAULT_ENGINE = SearchConfig(index_versions = ["stable", "dev"], engine = FlexSearch)
140+
const DEFAULT_ENGINE = SearchConfig(index_versions = ["stable", "dev"], engine = PageFind)
140141

141142
"""
142143
make(
@@ -246,13 +247,6 @@ function make(
246247
isdir(out_assets) || mkpath(out_assets)
247248
cp(joinpath(@__DIR__, "..", "assets", "default"), joinpath(out_assets, "default"))
248249

249-
if search_engine != false
250-
if search_engine.engine == Stork && !Stork.has_stork()
251-
@warn "stork binary not found. Falling back to flexsearch as search_engine."
252-
search_engine = DEFAULT_ENGINE
253-
end
254-
end
255-
256250
inject_styles_and_global_navigation(
257251
dir,
258252
docs,

src/search/pagefind.jl

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
module PageFind
2+
using NodeJS: NodeJS
3+
using HypertextLiteral
4+
5+
function inject_script!(custom_scripts, rootpath)
6+
pushfirst!(custom_scripts, joinpath("assets", "default", "pagefind_integration.js"))
7+
pushfirst!(custom_scripts, joinpath("pagefind", "pagefind.js"))
8+
end
9+
10+
function inject_styles!(custom_styles)
11+
pushfirst!(custom_styles, joinpath("assets", "default", "pagefind.css"))
12+
end
13+
14+
function render()
15+
return @htl """
16+
<div class="search nav-item">
17+
<input id="search-input" placeholder="Search everywhere...">
18+
<ol id="search-result-container" class="suggestions hidden">
19+
</ol>
20+
<div class="search-keybinding">/</div>
21+
</div>
22+
"""
23+
end
24+
25+
function build_search_index(root, docs, config, rootpath)
26+
if !success(`npx pagefind -V`)
27+
error("pagefind search engine not found. Aborting. Try running `npm install pagefind --global`")
28+
end
29+
30+
pattern = "*/{$(join(config.index_versions, ","))}/**/*.{html}"
31+
32+
run(`npx pagefind --site $(root) --glob $(pattern)`)
33+
end
34+
35+
end

0 commit comments

Comments
 (0)