Skip to content

Commit b2a87fc

Browse files
feat: add readthedocs javascript search
1 parent 28ed396 commit b2a87fc

File tree

5 files changed

+237
-3
lines changed

5 files changed

+237
-3
lines changed

docs/extensions.md

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,39 @@ Each item in the list must start with an element that has the class `tab-title`.
238238
</div>
239239
```
240240

241+
## Read the Docs search {#readthedocs-search}
242+
243+
Use search index from Read the Docs instead of the built-in doxygen search. This allows using search metrics from
244+
Read the Docs and in general gives a better search experience.
245+
246+
### Installation
247+
248+
1. Add the required resources in your `Doxyfile`:
249+
- **HTML_EXTRA_FILES:** `doxygen-awesome-readthedocs-search.js`
250+
- **HTML_EXTRA_STYLESHEET:** `doxygen-awesome-readthedocs-search.css`
251+
- **SEARCHENGINE:** `YES`
252+
- **SERVER_BASED_SEARCH:** `YES`
253+
- **EXTERNAL_SEARCH:** `YES`
254+
- **SEARCHENGINE_URL:** `https://<your-project>.readthedocs.io/` OR
255+
- **SEARCHENGINE_URL:** `https://<your-custom-readthedocs-domain>/`
256+
257+
`SEARCHENGINE_URL` is only used when testing locally, otherwise the domain name is detected automatically.
258+
When testing locally, search may not work without disabling CORS. This can be a security risk, so it is advised to
259+
only test inside of Read the Docs.
260+
261+
2. In the `header.html` template, include `doxygen-awesome-readthedocs-search.js` at the end of the `<head>` and then initialize it:
262+
```html
263+
<html>
264+
<head>
265+
<!-- ... other metadata & script includes ... -->
266+
<script type="text/javascript" src="$relpath^doxygen-awesome-readthedocs-search.js"></script>
267+
<script type="text/javascript">
268+
DoxygenAwesomeReadtheDocsSearch.init()
269+
</script>
270+
</head>
271+
<body>
272+
```
273+
241274
## Page Navigation {#extension-page-navigation}
242275

243276
@warning Experimental feature! Please report bugs [here](https://github.com/jothepro/doxygen-awesome-css/issues).
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
/* Highlight text in search results */
2+
span.highlighted {
3+
background-color:var(--warning-color);
4+
color: var(--page-foreground-color);
5+
padding: 2px;
6+
border-radius: 3px;
7+
}
8+
9+
/* Remove list bullets for search results */
10+
ul.search {
11+
list-style-type: none;
12+
padding: 0;
13+
}
14+
15+
/* Add horizontal divider for search results */
16+
ul.search li.search-result {
17+
border-bottom: 1px solid var(--separator-color);
18+
padding-bottom: 10px;
19+
margin-bottom: 10px;
20+
}
Lines changed: 172 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,172 @@
1+
class DoxygenAwesomeReadtheDocsSearch {
2+
static searchResultsText=[
3+
"Sorry, no pages matching your query.",
4+
"Search finished, found <b>1</b> page matching the search query.",
5+
"Search finished, found <b>$num</b> pages matching the search query.",
6+
];
7+
8+
static get serverUrl() {
9+
const serverUrlSuffix = '_/api/v3/';
10+
const domainName = window.location.hostname;
11+
console.log(`Domain name: ${domainName}`);
12+
13+
if (domainName === 'localhost') {
14+
let tmpServerUrl = serverUrl;
15+
while (tmpServerUrl.endsWith('/')) {
16+
tmpServerUrl = tmpServerUrl.slice(0, -1);
17+
}
18+
console.warn('Localhost detected, you probably need to bypass CORS');
19+
return `${tmpServerUrl}/${serverUrlSuffix}`;
20+
}
21+
return `https://${domainName}/${serverUrlSuffix}`;
22+
}
23+
24+
static init() {
25+
window.searchFor = function(query, page, count) {
26+
const results = $('#searchresults');
27+
28+
// Get the title
29+
let pageTitle = $('div.title')
30+
const originalTitle = pageTitle.text().toString();
31+
let pageTitleStates = ["Searching", "Searching .", "Searching ..", "Searching ..."];
32+
let pageTitleIndex = 0;
33+
34+
// Function to update the page title
35+
function updatePageTitle() {
36+
pageTitle.text(pageTitleStates[pageTitleIndex]);
37+
pageTitleIndex = (pageTitleIndex + 1) % pageTitleStates.length;
38+
}
39+
40+
// Start the interval to update the page title
41+
let titleInterval = setInterval(updatePageTitle, 500);
42+
43+
// The summary will be displayed at the top of the search results
44+
let resultSummary = document.createElement('p');
45+
resultSummary.className = 'search-summary';
46+
results.append(resultSummary);
47+
48+
// Put all results into an unordered list
49+
let resultList = document.createElement('ul');
50+
resultList.className = 'search';
51+
results.append(resultList);
52+
53+
// readthedocs metadata
54+
// TODO: how to handle defaults? ... only matters when this is outside of readthedocs
55+
let projectSlug = DoxygenAwesomeReadtheDocsSearch.getMetaValue("readthedocs-project-slug") || "doxygen-awesome-css";
56+
let projectVersion = DoxygenAwesomeReadtheDocsSearch.getMetaValue("readthedocs-version") || "latest";
57+
58+
// pull requests are not indexed, so use the default version
59+
if (/^\d+$/.test(projectVersion)) {
60+
console.log('Pull request detected, getting default version from ReadTheDocs API');
61+
DoxygenAwesomeReadtheDocsSearch.getReadTheDocsDefaultVersion(projectSlug);
62+
}
63+
64+
let url = `${DoxygenAwesomeReadtheDocsSearch.serverUrl}search/?q=project:${projectSlug}/${projectVersion}+${query}&page=${page + 1}&page_size=${count}`;
65+
console.log(url);
66+
67+
let firstUrl = true;
68+
69+
function fetchResults(url) {
70+
$.ajax({
71+
url: url,
72+
dataType: 'json',
73+
success: function (data) {
74+
// Add the query to the search field
75+
// This seems only be working if applied in the ajax success function...
76+
// maybe the field is not available before this point
77+
$('#MSearchField').val(query);
78+
79+
if (firstUrl) {
80+
if (data.count > 0) {
81+
if (data.count === 1) {
82+
resultSummary.innerHTML = DoxygenAwesomeReadtheDocsSearch.searchResultsText[1];
83+
} else {
84+
resultSummary.innerHTML = DoxygenAwesomeReadtheDocsSearch.searchResultsText[2].replace(/\$num/, data.count);
85+
}
86+
} else {
87+
resultSummary.innerHTML = DoxygenAwesomeReadtheDocsSearch.searchResultsText[0];
88+
}
89+
}
90+
91+
$.each(data.results, function (i, item) {
92+
let resultItem = document.createElement('li');
93+
resultItem.className = 'search-result';
94+
let resultItemUrl = `${item.domain}${item.path}`;
95+
let resultItemTitle = item.title;
96+
let resultItemType = item.type; // todo... we can possibly display results differently based on type
97+
let resultItemTitleLink = document.createElement('a');
98+
let resultItemTitleHeading = document.createElement('h3');
99+
resultItemTitleHeading.appendChild(resultItemTitleLink);
100+
resultItemTitleLink.href = resultItemUrl;
101+
resultItemTitleLink.textContent = resultItemTitle;
102+
resultItem.append(resultItemTitleHeading);
103+
resultList.append(resultItem);
104+
105+
let resultItemParagraph = document.createElement('p');
106+
resultItemParagraph.className = 'context';
107+
for (let i = 0; i < item.blocks.length; i++) {
108+
let blockContent = item.blocks[i].highlights.content.join(', ');
109+
110+
// Find all <span> tags and ensure they are highlighted
111+
blockContent = blockContent.replace(/<span>(.*?)<\/span>/g, '<span class="highlighted">$1</span>');
112+
resultItemParagraph.innerHTML += blockContent;
113+
114+
let blockName = `#${item.blocks[i].title.toLowerCase().replace(' ', '-')}`;
115+
let blockUrl = resultItemUrl + blockName;
116+
let blockLink = document.createElement('a');
117+
blockLink.href = blockUrl;
118+
blockLink.textContent = "More...";
119+
resultItemParagraph.append(document.createTextNode(' '));
120+
resultItemParagraph.append(blockLink);
121+
resultItemParagraph.append(document.createElement('br'));
122+
}
123+
resultItem.append(resultItemParagraph);
124+
});
125+
126+
// Add pagination
127+
firstUrl = false;
128+
if (data.next) {
129+
fetchResults(data.next);
130+
} else {
131+
// Clear the interval when the search is complete
132+
clearInterval(titleInterval);
133+
pageTitle.text(originalTitle);
134+
}
135+
}
136+
});
137+
}
138+
139+
fetchResults(url);
140+
}
141+
}
142+
143+
// Function to extract the value of a specified Read the Docs meta property
144+
static getMetaValue(propertyName) {
145+
const metaTags = document.getElementsByTagName('meta');
146+
147+
for (let meta of metaTags) {
148+
if (meta.name === propertyName) {
149+
return meta.content;
150+
}
151+
}
152+
153+
return null;
154+
}
155+
156+
static getReadTheDocsDefaultVersion(project) {
157+
let url = `${DoxygenAwesomeReadtheDocsSearch.serverUrl}projects/${project}/`;
158+
$.ajax({
159+
url: url,
160+
dataType: 'json',
161+
success: function(data) {
162+
console.log(data);
163+
return data.default_version;
164+
},
165+
error: function(jqXHR, textStatus, errorThrown) {
166+
console.error('Error:', textStatus, errorThrown);
167+
console.log(`Cannot determine default version for ${project}, assuming "latest"`);
168+
return "latest";
169+
}
170+
})
171+
}
172+
}

package-lock.json

Lines changed: 9 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@
2828
},
2929
"license": "MIT",
3030
"config": {},
31-
"dependencies": {},
32-
"devDependencies": {},
31+
"devDependencies": {
32+
"jquery": "^3.7.1"
33+
},
3334
"xpack": {}
3435
}

0 commit comments

Comments
 (0)