Skip to content

Commit

Permalink
feat: add readthedocs javascript search
Browse files Browse the repository at this point in the history
  • Loading branch information
ReenigneArcher committed Aug 7, 2024
1 parent 28ed396 commit b2a87fc
Show file tree
Hide file tree
Showing 5 changed files with 237 additions and 3 deletions.
33 changes: 33 additions & 0 deletions docs/extensions.md
Original file line number Diff line number Diff line change
Expand Up @@ -238,6 +238,39 @@ Each item in the list must start with an element that has the class `tab-title`.
</div>
```

## Read the Docs search {#readthedocs-search}

Use search index from Read the Docs instead of the built-in doxygen search. This allows using search metrics from
Read the Docs and in general gives a better search experience.

### Installation

1. Add the required resources in your `Doxyfile`:
- **HTML_EXTRA_FILES:** `doxygen-awesome-readthedocs-search.js`
- **HTML_EXTRA_STYLESHEET:** `doxygen-awesome-readthedocs-search.css`
- **SEARCHENGINE:** `YES`
- **SERVER_BASED_SEARCH:** `YES`
- **EXTERNAL_SEARCH:** `YES`
- **SEARCHENGINE_URL:** `https://<your-project>.readthedocs.io/` OR
- **SEARCHENGINE_URL:** `https://<your-custom-readthedocs-domain>/`

`SEARCHENGINE_URL` is only used when testing locally, otherwise the domain name is detected automatically.
When testing locally, search may not work without disabling CORS. This can be a security risk, so it is advised to
only test inside of Read the Docs.

2. In the `header.html` template, include `doxygen-awesome-readthedocs-search.js` at the end of the `<head>` and then initialize it:
```html
<html>
<head>
<!-- ... other metadata & script includes ... -->
<script type="text/javascript" src="$relpath^doxygen-awesome-readthedocs-search.js"></script>
<script type="text/javascript">
DoxygenAwesomeReadtheDocsSearch.init()
</script>
</head>
<body>
```

## Page Navigation {#extension-page-navigation}

@warning Experimental feature! Please report bugs [here](https://github.com/jothepro/doxygen-awesome-css/issues).
Expand Down
20 changes: 20 additions & 0 deletions doxygen-awesome-readthedocs-search.css
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/* Highlight text in search results */
span.highlighted {
background-color:var(--warning-color);
color: var(--page-foreground-color);
padding: 2px;
border-radius: 3px;
}

/* Remove list bullets for search results */
ul.search {
list-style-type: none;
padding: 0;
}

/* Add horizontal divider for search results */
ul.search li.search-result {
border-bottom: 1px solid var(--separator-color);
padding-bottom: 10px;
margin-bottom: 10px;
}
172 changes: 172 additions & 0 deletions doxygen-awesome-readthedocs-search.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,172 @@
class DoxygenAwesomeReadtheDocsSearch {
static searchResultsText=[
"Sorry, no pages matching your query.",
"Search finished, found <b>1</b> page matching the search query.",
"Search finished, found <b>$num</b> pages matching the search query.",
];

static get serverUrl() {
const serverUrlSuffix = '_/api/v3/';
const domainName = window.location.hostname;
console.log(`Domain name: ${domainName}`);

if (domainName === 'localhost') {
let tmpServerUrl = serverUrl;
while (tmpServerUrl.endsWith('/')) {
tmpServerUrl = tmpServerUrl.slice(0, -1);
}
console.warn('Localhost detected, you probably need to bypass CORS');
return `${tmpServerUrl}/${serverUrlSuffix}`;
}
return `https://${domainName}/${serverUrlSuffix}`;
}

static init() {
window.searchFor = function(query, page, count) {
const results = $('#searchresults');

// Get the title
let pageTitle = $('div.title')
const originalTitle = pageTitle.text().toString();
let pageTitleStates = ["Searching", "Searching .", "Searching ..", "Searching ..."];
let pageTitleIndex = 0;

// Function to update the page title
function updatePageTitle() {
pageTitle.text(pageTitleStates[pageTitleIndex]);
pageTitleIndex = (pageTitleIndex + 1) % pageTitleStates.length;
}

// Start the interval to update the page title
let titleInterval = setInterval(updatePageTitle, 500);

// The summary will be displayed at the top of the search results
let resultSummary = document.createElement('p');
resultSummary.className = 'search-summary';
results.append(resultSummary);

// Put all results into an unordered list
let resultList = document.createElement('ul');
resultList.className = 'search';
results.append(resultList);

// readthedocs metadata
// TODO: how to handle defaults? ... only matters when this is outside of readthedocs
let projectSlug = DoxygenAwesomeReadtheDocsSearch.getMetaValue("readthedocs-project-slug") || "doxygen-awesome-css";
let projectVersion = DoxygenAwesomeReadtheDocsSearch.getMetaValue("readthedocs-version") || "latest";

// pull requests are not indexed, so use the default version
if (/^\d+$/.test(projectVersion)) {
console.log('Pull request detected, getting default version from ReadTheDocs API');
DoxygenAwesomeReadtheDocsSearch.getReadTheDocsDefaultVersion(projectSlug);
}

let url = `${DoxygenAwesomeReadtheDocsSearch.serverUrl}search/?q=project:${projectSlug}/${projectVersion}+${query}&page=${page + 1}&page_size=${count}`;
console.log(url);

let firstUrl = true;

function fetchResults(url) {
$.ajax({
url: url,
dataType: 'json',
success: function (data) {
// Add the query to the search field
// This seems only be working if applied in the ajax success function...
// maybe the field is not available before this point
$('#MSearchField').val(query);

if (firstUrl) {
if (data.count > 0) {
if (data.count === 1) {
resultSummary.innerHTML = DoxygenAwesomeReadtheDocsSearch.searchResultsText[1];
} else {
resultSummary.innerHTML = DoxygenAwesomeReadtheDocsSearch.searchResultsText[2].replace(/\$num/, data.count);
}
} else {
resultSummary.innerHTML = DoxygenAwesomeReadtheDocsSearch.searchResultsText[0];
}
}

$.each(data.results, function (i, item) {
let resultItem = document.createElement('li');
resultItem.className = 'search-result';
let resultItemUrl = `${item.domain}${item.path}`;
let resultItemTitle = item.title;
let resultItemType = item.type; // todo... we can possibly display results differently based on type
let resultItemTitleLink = document.createElement('a');
let resultItemTitleHeading = document.createElement('h3');
resultItemTitleHeading.appendChild(resultItemTitleLink);
resultItemTitleLink.href = resultItemUrl;
resultItemTitleLink.textContent = resultItemTitle;
resultItem.append(resultItemTitleHeading);
resultList.append(resultItem);

let resultItemParagraph = document.createElement('p');
resultItemParagraph.className = 'context';
for (let i = 0; i < item.blocks.length; i++) {
let blockContent = item.blocks[i].highlights.content.join(', ');

// Find all <span> tags and ensure they are highlighted
blockContent = blockContent.replace(/<span>(.*?)<\/span>/g, '<span class="highlighted">$1</span>');
resultItemParagraph.innerHTML += blockContent;

let blockName = `#${item.blocks[i].title.toLowerCase().replace(' ', '-')}`;
let blockUrl = resultItemUrl + blockName;
let blockLink = document.createElement('a');
blockLink.href = blockUrl;
blockLink.textContent = "More...";
resultItemParagraph.append(document.createTextNode(' '));
resultItemParagraph.append(blockLink);
resultItemParagraph.append(document.createElement('br'));
}
resultItem.append(resultItemParagraph);
});

// Add pagination
firstUrl = false;
if (data.next) {
fetchResults(data.next);
} else {
// Clear the interval when the search is complete
clearInterval(titleInterval);
pageTitle.text(originalTitle);
}
}
});
}

fetchResults(url);
}
}

// Function to extract the value of a specified Read the Docs meta property
static getMetaValue(propertyName) {
const metaTags = document.getElementsByTagName('meta');

for (let meta of metaTags) {
if (meta.name === propertyName) {
return meta.content;
}
}

return null;
}

static getReadTheDocsDefaultVersion(project) {
let url = `${DoxygenAwesomeReadtheDocsSearch.serverUrl}projects/${project}/`;
$.ajax({
url: url,
dataType: 'json',
success: function(data) {
console.log(data);
return data.default_version;
},
error: function(jqXHR, textStatus, errorThrown) {
console.error('Error:', textStatus, errorThrown);
console.log(`Cannot determine default version for ${project}, assuming "latest"`);
return "latest";
}
})
}
}
10 changes: 9 additions & 1 deletion package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 3 additions & 2 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@
},
"license": "MIT",
"config": {},
"dependencies": {},
"devDependencies": {},
"devDependencies": {
"jquery": "^3.7.1"
},
"xpack": {}
}

0 comments on commit b2a87fc

Please sign in to comment.