Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Allow to enable server side search for MkDocs #6986

Merged
merged 16 commits into from Jun 4, 2020
Merged
174 changes: 169 additions & 5 deletions readthedocs/core/static-src/core/js/doc-embed/search.js
@@ -1,5 +1,5 @@
/*
* Sphinx search overrides
* Sphinx and Mkdocs search overrides
*/

var rtddata = require('./rtd-data');
Expand Down Expand Up @@ -40,7 +40,7 @@ function append_html_to_contents(contents, template, data) {
* Sphinx indexer. This will fall back to the standard indexer on an API
* failure,
*/
function attach_elastic_search_query(data) {
function attach_elastic_search_query_sphinx(data) {
var project = data.project;
var version = data.version;
var language = data.language || 'en';
Expand All @@ -56,7 +56,6 @@ function attach_elastic_search_query(data) {
search_def
.then(function (data) {
var hit_list = data.results || [];
var total_count = data.count || 0;

if (hit_list.length) {
for (var i = 0; i < hit_list.length; i += 1) {
Expand Down Expand Up @@ -265,12 +264,13 @@ function attach_elastic_search_query(data) {
};

if (typeof Search !== 'undefined' && project && version) {

// Do not replace the built-in search if RTD's docsearch is disabled
if (!data.features || !data.features.docsearch_disabled) {
var query_fallback = Search.query;
Search.query_fallback = query_fallback;
Search.query = query_override;
} else {
console.log('Server side search is disabled.');
}
}
$(document).ready(function () {
Expand All @@ -281,9 +281,173 @@ function attach_elastic_search_query(data) {
}


/*
* Mkdocs search override for hitting our API instead of the standard Mkdocs search index.
* This will fall back to the original search on an API failure.
*/
function attach_elastic_search_query_mkdocs(data) {
var project = data.project;
var version = data.version;
var language = data.language || 'en';

var fallbackSearch = function () {
if (typeof window.doSearchFallback !== 'undefined') {
window.doSearchFallback();
} else {
console.log('Unable to fallback to original MkDocs search.');
}
};

var doSearch = function () {
var query = document.getElementById('mkdocs-search-query').value;

var search_def = $.Deferred();

var search_url = document.createElement('a');
search_url.href = data.proxied_api_host + '/api/v2/docsearch/';
search_url.search = '?q=' + encodeURIComponent(query) + '&project=' + project +
'&version=' + version + '&language=' + language;

search_def
.then(function (data) {
var hit_list = data.results || [];

if (hit_list.length) {
var searchResults = $('#mkdocs-search-results');
searchResults.empty();

for (var i = 0; i < hit_list.length; i += 1) {
var doc = hit_list[i];
var inner_hits = doc.inner_hits || [];

var result = $('<article>');
result.append(
$('<h3>').append($('<a>', {'href': doc.link, 'text': doc.title}))
);

if (doc.project !== project) {
var text = '(from project ' + doc.project + ')';
result.append($('<span>', {'text': text}));
}

for (var j = 0; j < inner_hits.length; j += 1) {
var section = inner_hits[j];

if (section.type === 'sections') {
var section_link = doc.link + '#' + section._source.id;
var section_title = section._source.title;
var section_content = section._source.content;
if (section_content.length > MAX_SUBSTRING_LIMIT) {
section_content = section_content.substr(0, MAX_SUBSTRING_LIMIT) + " ...";
}
var section_contents = [section_content];

if (section.highlight) {
if (section.highlight["sections.title"]) {
section_title = section.highlight["sections.title"][0];
}
if (section.highlight["sections.content"]) {
var contents = section.highlight["sections.content"];
section_contents = [];
for (
var k = 0;
k < contents.length && k < MAX_RESULT_PER_SECTION;
k += 1
) {
section_contents.push("... " + contents[k] + " ...");
}
}
}

result.append(
$('<h4>')
.append($('<a>', {'href': section_link}).html(xss(section_title)))
);
for (var m = 0; m < section_contents.length; m += 1) {
var content = xss(section_contents[m]);
result.append(
$('<p>').html(section_contents)
);
}
searchResults.append(result);
}
}
}
} else {
console.log('Read the Docs search returned 0 result. Falling back to MkDocs search.');
fallbackSearch();
}
})
.fail(function (error) {
console.log('Read the Docs search failed. Falling back to MkDocs search.');
fallbackSearch();
});

$.ajax({
url: search_url.href,
crossDomain: true,
xhrFields: {
withCredentials: true,
},
complete: function (resp, status_code) {
if (
status_code !== 'success' ||
typeof (resp.responseJSON) === 'undefined' ||
resp.responseJSON.count === 0
) {
return search_def.reject();
}
return search_def.resolve(resp.responseJSON);
}
})
.fail(function (resp, status_code, error) {
return search_def.reject();
});
};

var initSearch = function () {
var search_input = document.getElementById('mkdocs-search-query');
if (search_input) {
search_input.addEventListener('keyup', doSearch);
}

var term = window.getSearchTermFromLocation();
if (term) {
search_input.value = term;
doSearch();
}
};

$(document).ready(function () {
// We can't override the search completely,
// because we can't delete the original event listener,
// and MkDocs includes its search functions after ours.
// If MkDocs is loaded before, this will trigger a double search
// (but ours will have precendece).

// Note: this function is only available on Mkdocs >=1.x
window.doSearchFallback = window.doSearch;

window.doSearch = doSearch;
window.initSearch = initSearch;
initSearch();
});
}


function init() {
var data = rtddata.get();
attach_elastic_search_query(data);
if (data.is_sphinx_builder()) {
// Check to disabled server side search for Sphinx
// happens inside the function, because we still need to call Search.init().
attach_elastic_search_query_sphinx(data);
}
// MkDocs projects should have this flag explicitly for now.
else if (data.features && !data.features.docsearch_disabled) {
ericholscher marked this conversation as resolved.
Show resolved Hide resolved
attach_elastic_search_query_mkdocs(data);
} else {
console.log('Server side search is disabled.');
}
}

module.exports = {
Expand Down
2 changes: 1 addition & 1 deletion readthedocs/core/static/core/js/readthedocs-doc-embed.js

Large diffs are not rendered by default.

6 changes: 6 additions & 0 deletions readthedocs/doc_builder/backends/mkdocs.py
Expand Up @@ -253,6 +253,12 @@ def generate_rtd_data(self, docs_dir, mkdocs_config):
'commit': self.version.project.vcs_repo(self.version.slug).commit,
'global_analytics_code': settings.GLOBAL_ANALYTICS_CODE,
'user_analytics_code': analytics_code,
'features': {
'docsearch_disabled': (
not self.project.has_feature(Feature.ENABLE_MKDOCS_SERVER_SIDE_SEARCH)
or self.project.has_feature(Feature.DISABLE_SERVER_SIDE_SEARCH)
)
},
}

data_ctx = {
Expand Down
1 change: 1 addition & 0 deletions readthedocs/doc_builder/backends/sphinx.py
Expand Up @@ -181,6 +181,7 @@ def get_config_params(self):
'dont_overwrite_sphinx_context': self.project.has_feature(
Feature.DONT_OVERWRITE_SPHINX_CONTEXT,
),
'docsearch_disabled': self.project.has_feature(Feature.DISABLE_SERVER_SIDE_SEARCH),
}

finalize_sphinx_context_data.send(
Expand Down
1 change: 1 addition & 0 deletions readthedocs/doc_builder/templates/doc_builder/conf.py.tmpl
Expand Up @@ -114,6 +114,7 @@ context = {
'new_theme': (html_theme == "sphinx_rtd_theme"),
'source_suffix': SUFFIX,
'ad_free': {% if project.show_advertising %}False{% else %}True{% endif %},
'docsearch_disabled': {{ docsearch_disabled }},
'user_analytics_code': '{{ project.analytics_code|default_if_none:'' }}',
'global_analytics_code': '{{ settings.GLOBAL_ANALYTICS_CODE }}',
'commit': {% if project.repo_type == 'git' %}'{{ commit|slice:"8" }}'{% else %}'{{ commit }}'{% endif %},
Expand Down
10 changes: 10 additions & 0 deletions readthedocs/projects/models.py
Expand Up @@ -1590,6 +1590,8 @@ def add_features(sender, **kwargs):
SKIP_SYNC_BRANCHES = 'skip_sync_branches'
CACHED_ENVIRONMENT = 'cached_environment'
LIMIT_CONCURRENT_BUILDS = 'limit_concurrent_builds'
DISABLE_SERVER_SIDE_SEARCH = 'disable_server_side_search'
ENABLE_MKDOCS_SERVER_SIDE_SEARCH = 'enable_mkdocs_server_side_search'
FORCE_SPHINX_FROM_VENV = 'force_sphinx_from_venv'
LIST_PACKAGES_INSTALLED_ENV = 'list_packages_installed_env'
VCS_REMOTE_LISTING = 'vcs_remote_listing'
Expand Down Expand Up @@ -1669,6 +1671,14 @@ def add_features(sender, **kwargs):
LIMIT_CONCURRENT_BUILDS,
_('Limit the amount of concurrent builds'),
),
(
DISABLE_SERVER_SIDE_SEARCH,
_('Disable server side search'),
),
(
ENABLE_MKDOCS_SERVER_SIDE_SEARCH,
_('Enable server side search for MkDocs projects'),
),
(
FORCE_SPHINX_FROM_VENV,
_('Force to use Sphinx from the current virtual environment'),
Expand Down