Skip to content
This repository was archived by the owner on Apr 8, 2025. It is now read-only.

Commit f36ee3d

Browse files
committed
Use API v3
1 parent c8cb912 commit f36ee3d

File tree

6 files changed

+172
-44
lines changed

6 files changed

+172
-44
lines changed

.readthedocs.yaml

+2-6
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,9 @@
11
version: 2
22

33
build:
4-
os: "ubuntu-20.04"
4+
os: "ubuntu-22.04"
55
tools:
6-
# sphinx-js isn't compatible with python 3.10.
7-
# https://github.com/mozilla/sphinx-js/issues/186
8-
python: "3.9"
6+
python: "3.11"
97
nodejs: "16"
108

119
python:
@@ -16,5 +14,3 @@ python:
1614

1715
sphinx:
1816
configuration: docs/conf.py
19-
20-
formats: []

docs/conf.py

+3-2
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@
1111
# documentation root, use os.path.abspath to make it absolute, like shown here.
1212
#
1313
import os
14+
from pathlib import Path
1415
import sys
15-
sys.path.insert(0, os.path.abspath('..'))
16-
sys.path.append(os.path.abspath("./_ext"))
16+
sys.path.insert(0, str(Path(__file__).parent.parent))
17+
sys.path.insert(0, str(Path(__file__).parent / "_ext"))
1718

1819
ON_RTD = os.environ.get('READTHEDOCS', False)
1920

sphinx_search/extension.py

+41-8
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,18 @@
1-
import os
2-
31
from sphinx_search import __version__
42
from sphinx.errors import ExtensionError
3+
from pathlib import Path
54
from sphinx.util.fileutil import copy_asset
65

76
ASSETS_FILES = {
87
'minified': [
9-
os.path.join('js', 'rtd_sphinx_search.min.js'),
10-
os.path.join('css', 'rtd_sphinx_search.min.css'),
8+
Path("js/rtd_search_config.js_t"),
9+
Path("js/rtd_sphinx_search.min.js"),
10+
Path("css/rtd_sphinx_search.min.css"),
1111
],
1212
'un-minified': [
13-
os.path.join('js', 'rtd_sphinx_search.js'),
14-
os.path.join('css', 'rtd_sphinx_search.css'),
13+
Path("js/rtd_search_config.js_t"),
14+
Path("js/rtd_sphinx_search.js"),
15+
Path("css/rtd_sphinx_search.css"),
1516
]
1617
}
1718

@@ -24,16 +25,39 @@ def _get_static_files(config):
2425
return ASSETS_FILES[file_type]
2526

2627

28+
def get_context(config):
29+
filters = config.rtd_sphinx_search_filters.copy()
30+
default_filter = filters.pop("default", "")
31+
# When converting to JSON, the order of the keys is not guaranteed.
32+
filters = [(name, filter) for name, filter in filters.items()]
33+
return {
34+
"rtd_search_config": {
35+
"filters": filters,
36+
"default_filter": default_filter,
37+
}
38+
}
39+
40+
2741
def copy_asset_files(app, exception):
2842
if exception is None: # build succeeded
43+
root = Path(__file__).parent
2944
for file in _get_static_files(app.config):
30-
path = os.path.join(os.path.dirname(__file__), 'static', file)
31-
copy_asset(path, os.path.join(app.outdir, '_static', file.split('.')[-1]))
45+
source = root / 'static' / file
46+
destination = Path(app.outdir) / '_static' / file.parent
47+
context = None
48+
# If the file ends with _t, it is a template file,
49+
# so we provide a context to treat it as a template.
50+
if file.name.endswith('_t'):
51+
context = get_context(app.config)
52+
copy_asset(str(source), str(destination), context=context)
3253

3354

3455
def inject_static_files(app):
3556
"""Inject correct CSS and JS files based on the value of ``rtd_sphinx_search_file_type``."""
3657
for file in _get_static_files(app.config):
58+
file = str(file)
59+
if file.endswith('_t'):
60+
file = file[:-2]
3761
if file.endswith('.js'):
3862
app.add_js_file(file)
3963
elif file.endswith('.css'):
@@ -43,6 +67,15 @@ def inject_static_files(app):
4367
def setup(app):
4468

4569
app.add_config_value('rtd_sphinx_search_file_type', 'minified', 'html')
70+
app.add_config_value(
71+
'rtd_sphinx_search_filters',
72+
{
73+
"default": "project:@this",
74+
"Search this project": "project:@this",
75+
"Search subprojects": "subprojects:@this",
76+
},
77+
'html',
78+
)
4679

4780
app.connect('builder-inited', inject_static_files)
4881
app.connect('build-finished', copy_asset_files)

sphinx_search/static/css/rtd_sphinx_search.css

+11
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,17 @@
282282
letter-spacing: 1px;
283283
}
284284

285+
.search__filters ul {
286+
list-style: none;
287+
padding: 0;
288+
margin: 0;
289+
}
290+
291+
.search__filters li {
292+
display: inline;
293+
margin-right: 0.5em;
294+
}
295+
285296
@media (max-width: 670px) {
286297
.rtd__search__credits {
287298
height: 50px;
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
var RTD_SEARCH_CONFIG = {{ rtd_search_config | tojson }};

sphinx_search/static/js/rtd_sphinx_search.js

+114-28
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,26 @@ const FETCH_RESULTS_DELAY = 250;
66
const CLEAR_RESULTS_DELAY = 300;
77
const RTD_SEARCH_PARAMETER = "rtd_search";
88

9+
function parseQuery(search_query) {
10+
let query = [];
11+
let options = {};
12+
search_query.split(/\s+/).forEach(function (term) {
13+
let result = term.split(":", 2);
14+
if (result.length < 2) {
15+
query.push(term);
16+
} else {
17+
if (result[0] in options) {
18+
options[result[0]].push(result[1]);
19+
} else {
20+
options[result[0]] = [result[1]];
21+
}
22+
}
23+
});
24+
query = query.join(" ");
25+
return [query, options];
26+
}
27+
28+
929
/**
1030
* Debounce the function.
1131
* Usage::
@@ -111,6 +131,12 @@ const updateSearchBar = () => {
111131
};
112132

113133

134+
function setSearchQuery(query) {
135+
let search_outer_input = document.querySelector(".search__outer__input");
136+
search_outer_input.value = query;
137+
}
138+
139+
114140
/*
115141
* Returns true if the modal window is visible.
116142
*/
@@ -273,9 +299,10 @@ const generateSingleResult = (resultData, projectName, id) => {
273299

274300
// If the result is not from the same project,
275301
// then it's from a subproject.
276-
if (projectName !== resultData.project) {
302+
const project_slug = resultData.project.slug
303+
if (projectName !== project_slug) {
277304
let subtitle = createDomNode("small", {class: "rtd_ui_search_subtitle"});
278-
subtitle.innerText = `(from project ${resultData.project})`;
305+
subtitle.innerText = ` (from project ${project_slug})`;
279306
h2_element.appendChild(subtitle);
280307
}
281308
h2_element.appendChild(createDomNode("br"))
@@ -489,7 +516,7 @@ const getErrorDiv = err_msg => {
489516
* @param {String} projectName: name (slug) of the project
490517
* @return {Function} debounced function with debounce time of 500ms
491518
*/
492-
const fetchAndGenerateResults = (api_endpoint, parameters, projectName) => {
519+
const fetchAndGenerateResults = (api_endpoint, parameters) => {
493520
let search_outer = document.querySelector(".search__outer");
494521

495522
// Removes all results (if there is any),
@@ -518,7 +545,7 @@ const fetchAndGenerateResults = (api_endpoint, parameters, projectName) => {
518545
if (data.results.length > 0) {
519546
let search_result_box = generateSuggestionsList(
520547
data,
521-
projectName
548+
parameters.project
522549
);
523550
removeResults();
524551
search_outer.appendChild(search_result_box);
@@ -550,28 +577,56 @@ const fetchAndGenerateResults = (api_endpoint, parameters, projectName) => {
550577
* This html structure will serve as the boilerplate
551578
* to show our search results.
552579
*
580+
* @param {Array} filters: filters to be applied to the search
553581
* @return {String} initial html structure
554582
*/
555-
const generateAndReturnInitialHtml = () => {
556-
let innerHTML =
557-
'<div class="search__outer"> \
558-
<div class="search__cross" title="Close"> \
559-
<!--?xml version="1.0" encoding="UTF-8"?--> \
560-
<svg class="search__cross__img" width="15px" height="15px" enable-background="new 0 0 612 612" version="1.1" viewBox="0 0 612 612" xml:space="preserve" xmlns="http://www.w3.org/2000/svg"> \
561-
<polygon points="612 36.004 576.52 0.603 306 270.61 35.478 0.603 0 36.004 270.52 306.01 0 576 35.478 611.4 306 341.41 576.52 611.4 612 576 341.46 306.01"></polygon> \
562-
</svg> \
563-
</div> \
564-
<input class="search__outer__input" placeholder="Search ..."> \
565-
<span class="bar"></span> \
566-
</div> \
567-
<div class="rtd__search__credits"> \
568-
Search by <a href="https://readthedocs.org/">Read the Docs</a> & <a href="https://readthedocs-sphinx-search.readthedocs.io/en/latest/">readthedocs-sphinx-search</a> \
569-
</div>';
583+
const generateAndReturnInitialHtml = (filters) => {
584+
let innerHTML = `
585+
<div class="search__outer">
586+
<div class="search__cross" title="Close">
587+
<!--?xml version="1.0" encoding="UTF-8"?-->
588+
<svg class="search__cross__img" width="15px" height="15px" enable-background="new 0 0 612 612" version="1.1" viewBox="0 0 612 612" xml:space="preserve" xmlns="http://www.w3.org/2000/svg">
589+
<polygon points="612 36.004 576.52 0.603 306 270.61 35.478 0.603 0 36.004 270.52 306.01 0 576 35.478 611.4 306 341.41 576.52 611.4 612 576 341.46 306.01"></polygon>
590+
</svg>
591+
</div>
592+
<input class="search__outer__input" placeholder="Search...">
593+
<div class="bar"></div>
594+
<div class="search__filters">
595+
<ul>
596+
</ul>
597+
</div>
598+
</div>
599+
<div class="rtd__search__credits">
600+
Search by <a href="https://readthedocs.org/">Read the Docs</a> & <a href="https://readthedocs-sphinx-search.readthedocs.io/en/latest/">readthedocs-sphinx-search</a>
601+
</div>
602+
`;
570603

571604
let div = createDomNode("div", {
572605
class: "search__outer__wrapper search__backdrop",
573606
});
574607
div.innerHTML = innerHTML;
608+
let filters_list = div.querySelector(".search__filters ul");
609+
const config = getConfig();
610+
for (const [filter, value] of filters) {
611+
let li = createDomNode("li");
612+
let button = createDomNode("button");
613+
button.innerText = filter;
614+
button.value = value;
615+
button.addEventListener("click", event => {
616+
event.preventDefault();
617+
let search_query = getSearchTerm();
618+
let [query, _] = parseQuery(search_query);
619+
setSearchQuery(event.target.value + " " + query);
620+
const search_params = {
621+
q: search_query,
622+
project: config.project,
623+
version: config.version,
624+
};
625+
fetchAndGenerateResults(config.api_endpoint, search_params)();
626+
});
627+
li.appendChild(button);
628+
filters_list.appendChild(li);
629+
}
575630
return div;
576631
};
577632

@@ -609,7 +664,7 @@ const showSearchModal = custom_query => {
609664
search_outer_input.focus();
610665
}
611666
};
612-
667+
613668
if (window.jQuery) {
614669
$(".search__outer__wrapper").fadeIn(ANIMATION_TIME, show_modal);
615670
} else {
@@ -650,15 +705,43 @@ const removeSearchModal = () => {
650705
}
651706
};
652707

708+
709+
/**
710+
* Get the config used by the search.
711+
*
712+
* This configiration is extracted from the global variable
713+
* READTHEDOCS_DATA, which is defined by Read the Docs,
714+
* and the global variable RTD_SEARCH_CONFIG, which is defined
715+
* by the sphinx extension.
716+
*
717+
* @return {Object} config
718+
*/
719+
function getConfig() {
720+
const project = READTHEDOCS_DATA.project;
721+
const version = READTHEDOCS_DATA.version;
722+
const api_host = READTHEDOCS_DATA.proxied_api_host || '/_';
723+
// This variable is defined in another file.
724+
// that is loaded before this file,
725+
// containing settings from the sphinx extension.
726+
const search_config = RTD_SEARCH_CONFIG || {};
727+
const api_endpoint = api_host + "/api/v3/search/";
728+
return {
729+
project: project,
730+
version: version,
731+
api_endpoint: api_endpoint,
732+
filters: search_config.filters,
733+
default_filter: search_config.default_filter,
734+
}
735+
}
736+
737+
653738
window.addEventListener("DOMContentLoaded", () => {
654739
// only add event listeners if READTHEDOCS_DATA global
655740
// variable is found.
656741
if (window.hasOwnProperty("READTHEDOCS_DATA")) {
657-
const project = READTHEDOCS_DATA.project;
658-
const version = READTHEDOCS_DATA.version;
659-
const api_host = READTHEDOCS_DATA.proxied_api_host || '/_';
742+
const config = getConfig();
660743

661-
let initialHtml = generateAndReturnInitialHtml();
744+
let initialHtml = generateAndReturnInitialHtml(config.filters);
662745
document.body.appendChild(initialHtml);
663746

664747
let search_outer_wrapper = document.querySelector(
@@ -684,13 +767,16 @@ window.addEventListener("DOMContentLoaded", () => {
684767
// cancel previous ajax request.
685768
current_request.cancel();
686769
}
687-
const search_endpoint = api_host + "/api/v2/search/";
770+
let [query, options] = parseQuery(search_query);
771+
if (Object.keys(options).length == 0) {
772+
search_query = config.default_filter + " " + query;
773+
}
688774
const search_params = {
689775
q: search_query,
690-
project: project,
691-
version: version,
776+
project: config.project,
777+
version: config.version,
692778
};
693-
current_request = fetchAndGenerateResults(search_endpoint, search_params, project);
779+
current_request = fetchAndGenerateResults(config.api_endpoint, search_params);
694780
current_request();
695781
} else {
696782
// if the last request returns the results,

0 commit comments

Comments
 (0)