diff --git a/MANIFEST.in b/MANIFEST.in index abbb0bc..0f9860b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -1,4 +1,6 @@ prune common include LICENSE +include sphinx_search/_static/js/rtd_sphinx_search.js include sphinx_search/_static/js/rtd_sphinx_search.min.js +include sphinx_search/_static/css/rtd_sphinx_search.css include sphinx_search/_static/css/rtd_sphinx_search.min.css diff --git a/docs/conf.py b/docs/conf.py index 390185f..c3de4fb 100644 --- a/docs/conf.py +++ b/docs/conf.py @@ -176,3 +176,9 @@ # A list of files that should not be packed into the epub file. epub_exclude_files = ['search.html'] + +# -- Setup for 'confval' used in docs/configuration.rst ---------------------- + +def setup(app): + app.add_object_type('confval', 'confval', + 'pair: %s; configuration value') diff --git a/docs/configuration.rst b/docs/configuration.rst new file mode 100644 index 0000000..fbe9981 --- /dev/null +++ b/docs/configuration.rst @@ -0,0 +1,18 @@ +Configuration +============= + +The following settings are available. +You can customize these configuration options in your ``conf.py`` file: + +.. confval:: rtd_sphinx_search_file_type + + Description: Type of files to be included in the html. + + Possible values: + + - ``minified``: Include the minified and uglified CSS and JS files. + - ``un-minified``: Include the original CSS and JS files. + + Default: ``'minified'`` + + Type: ``string`` diff --git a/docs/custom-design.rst b/docs/custom-design.rst index 19b2cf8..4c15d22 100644 --- a/docs/custom-design.rst +++ b/docs/custom-design.rst @@ -4,49 +4,12 @@ Custom Design If you want to change the styles of the search UI, you can do so by `adding your custom stylesheet`_ to your documentation. -Basic structure of the HTML which is generated for the full-page search UI +Basic structure of the HTML which is generated for the search UI is given below for the reference: .. code-block:: html -
-
- - -
- - - - - -
- - - - -
- - -
- + .. _adding your custom stylesheet: https://docs.readthedocs.io/page/guides/adding-custom-css.html diff --git a/docs/index.rst b/docs/index.rst index a27aadc..5f3ba38 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -39,6 +39,7 @@ The CSS is also autoprefixed to extend the support to most of the browsers. :caption: Table of Contents installation + configuration custom-design development testing diff --git a/gulpfile.js b/gulpfile.js index de9b74c..4aa7d70 100644 --- a/gulpfile.js +++ b/gulpfile.js @@ -11,7 +11,7 @@ var autoprefixer = require("gulp-autoprefixer"), gulp.task("styles", function() { return gulp - .src("sphinx_search/_static/css/*.css") + .src("sphinx_search/_static/css/rtd_sphinx_search.css") .pipe(autoprefixer()) .pipe(csso()) .pipe(rename({ extname: ".min.css" })) @@ -20,7 +20,7 @@ gulp.task("styles", function() { gulp.task("scripts", function() { return gulp - .src("sphinx_search/_static/js/*.js") + .src("sphinx_search/_static/js/rtd_sphinx_search.js") .pipe(babel({ presets: ["@babel/env"] })) .pipe(uglify()) .pipe(rename({ extname: ".min.js" })) @@ -28,7 +28,7 @@ gulp.task("scripts", function() { }); gulp.task("clean", function() { - return del(["sphinx_search/**/*.min.*"]); + return del(["sphinx_search/**/rtd_sphinx_search.min.*"]); }); gulp.task("default", ["clean"], function() { diff --git a/sphinx_search/_static/css/rtd_sphinx_search.css b/sphinx_search/_static/css/rtd_sphinx_search.css index 1b18c36..8ad456f 100644 --- a/sphinx_search/_static/css/rtd_sphinx_search.css +++ b/sphinx_search/_static/css/rtd_sphinx_search.css @@ -61,6 +61,7 @@ .search__outer::-webkit-scrollbar { width: 7px; + height: 7px; background-color: #fcfcfc; } @@ -149,7 +150,8 @@ /* Search result */ .search__result__single { - padding: 10px; + margin-top: 10px; + padding: 0px 10px; border-bottom: 1px solid #e6e6e6; } @@ -157,8 +159,10 @@ background-color: rgb(245, 245, 245); } -.search__result__single:hover { - background-color: rgb(245, 245, 245); +.outer_div_page_results { + margin: 5px 0px; + overflow: auto; + padding: 3px 5px; } .search__result__single a { @@ -172,30 +176,37 @@ /* Display and box model */ display: inline-block; font-weight: 500; - margin-bottom: 0; + margin-bottom: 15px; margin-top: 0; - font-size: 14px; + font-size: 15px; /* Other */ color: #6ea0ec; border-bottom: 1px solid #6ea0ec; } -/* Path of each search result */ - -.search__result__path { - color: #b3b3b3; +.search__result__subheading { + color: black; + font-weight: 700; + float: left; + width: 20%; + font-size: 15px; + margin-right: 10px; + overflow-x: hidden; } -/* Content of each search result */ - .search__result__content { + margin: 0; text-decoration: none; color: black; - font-size: 14px; + font-size: 15px; display: block; - margin-top: 3px; margin-bottom: 5px; + margin-bottom: 0; + line-height: inherit; + float: right; + width: calc(80% - 15px); + text-align: left; } /* Highlighting of matched results */ @@ -206,7 +217,7 @@ .search__outer .search__result__title em { background-color: #e5f6ff; - padding-bottom: 4px; + padding-bottom: 3px; border-bottom-color: black; } @@ -215,6 +226,38 @@ border-bottom: 1px solid black; } +.search__result__subheading em { + border-bottom: 1px solid black; +} + +.outer_div_page_results:hover { + background-color: rgb(245, 245, 245); +} + +.br-for-hits { + display: block; + content: ""; + margin-top: 10px; +} + +.rtd_ui_search_subtitle { + all: unset; + color: inherit; + font-size: 85%; +} + +@media (max-width: 630px) { + .search__result__subheading { + float: none; + width: 90%; + } + + .search__result__content { + float: none; + width: 90%; + } +} + @keyframes fade-in { from { opacity: 0; diff --git a/sphinx_search/_static/css/rtd_sphinx_search.min.css b/sphinx_search/_static/css/rtd_sphinx_search.min.css index b12759c..8b08e79 100644 --- a/sphinx_search/_static/css/rtd_sphinx_search.min.css +++ b/sphinx_search/_static/css/rtd_sphinx_search.min.css @@ -1 +1 @@ -@-webkit-keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.search__backdrop,.search__outer__wrapper{position:fixed;top:0;left:0;width:100%;height:100%;z-index:700}.search__backdrop{z-index:500;display:none;background-color:rgba(0,0,0,.502)}.search__outer{margin:auto;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100000;height:80%;width:80%;max-height:1000px;max-width:1500px;padding:10px;overflow-y:scroll;border:1px solid #e0e0e0;border-radius:5px;line-height:1.875;background-color:#fcfcfc;-webkit-box-shadow:1px 3px 4px rgba(0,0,0,.09);box-shadow:1px 3px 4px rgba(0,0,0,.09);text-align:left}.search__outer::-webkit-scrollbar-track{border-radius:10px;background-color:#fcfcfc}.search__outer::-webkit-scrollbar{width:7px;background-color:#fcfcfc}.search__outer::-webkit-scrollbar-thumb{border-radius:10px;background-color:#8f8f8f}.search__cross__img{width:15px;height:15px;margin:12px}.search__cross{position:absolute;top:0;right:0}.search__cross:hover{cursor:pointer}.search__outer__input{width:90%;height:30px;font-size:19px;outline:0;background-color:#fcfcfc;border:0;border-bottom:1px solid #757575;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgNDUxIDQ1MSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDUxIDQ1MTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQoJPHBhdGggZD0iTTQ0Ny4wNSw0MjhsLTEwOS42LTEwOS42YzI5LjQtMzMuOCw0Ny4yLTc3LjksNDcuMi0xMjYuMUMzODQuNjUsODYuMiwyOTguMzUsMCwxOTIuMzUsMEM4Ni4yNSwwLDAuMDUsODYuMywwLjA1LDE5Mi4zDQoJCXM4Ni4zLDE5Mi4zLDE5Mi4zLDE5Mi4zYzQ4LjIsMCw5Mi4zLTE3LjgsMTI2LjEtNDcuMkw0MjguMDUsNDQ3YzIuNiwyLjYsNi4xLDQsOS41LDRzNi45LTEuMyw5LjUtNA0KCQlDNDUyLjI1LDQ0MS44LDQ1Mi4yNSw0MzMuMiw0NDcuMDUsNDI4eiBNMjYuOTUsMTkyLjNjMC05MS4yLDc0LjItMTY1LjMsMTY1LjMtMTY1LjNjOTEuMiwwLDE2NS4zLDc0LjIsMTY1LjMsMTY1LjMNCgkJcy03NC4xLDE2NS40LTE2NS4zLDE2NS40QzEwMS4xNSwzNTcuNywyNi45NSwyODMuNSwyNi45NSwxOTIuM3oiLz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjwvc3ZnPg0K);background-repeat:no-repeat;background-position:left;background-size:15px;padding-left:25px}.search__outer__input:focus{outline:0}.search__outer .bar{position:relative;display:block;width:90%;margin-bottom:15px}.search__outer .bar:after,.search__outer .bar:before{content:"";height:2px;width:0;bottom:1px;position:absolute;background:#5264ae;-webkit-transition:.2s ease all;-o-transition:.2s ease all;transition:.2s ease all}.search__outer .bar:before{left:50%}.search__outer .bar:after{right:50%}.search__outer__input:focus~.bar:after,.search__outer__input:focus~.bar:before{width:50%}.search__result__single{padding:10px;border-bottom:1px solid #e6e6e6}.search__result__box .active,.search__result__single:hover{background-color:#f5f5f5}.search__result__single a{text-decoration:none;cursor:pointer}.search__result__title{display:inline-block;font-weight:500;margin-bottom:0;margin-top:0;font-size:14px;color:#6ea0ec;border-bottom:1px solid #6ea0ec}.search__result__path{color:#b3b3b3}.search__result__content{text-decoration:none;color:#000;font-size:14px;display:block;margin-top:3px;margin-bottom:5px}.search__outer em{font-style:normal}.search__outer .search__result__title em{background-color:#e5f6ff;padding-bottom:4px;border-bottom-color:#000}.search__outer .search__result__content em{background-color:#e5f6ff;border-bottom:1px solid #000} \ No newline at end of file +@-webkit-keyframes fade-in{0%{opacity:0}to{opacity:1}}@keyframes fade-in{0%{opacity:0}to{opacity:1}}.search__backdrop,.search__outer__wrapper{position:fixed;top:0;left:0;width:100%;height:100%;z-index:700}.search__backdrop{z-index:500;display:none;background-color:rgba(0,0,0,.502)}.search__outer{margin:auto;position:absolute;top:0;left:0;right:0;bottom:0;z-index:100000;height:80%;width:80%;max-height:1000px;max-width:1500px;padding:10px;overflow-y:scroll;border:1px solid #e0e0e0;border-radius:5px;line-height:1.875;background-color:#fcfcfc;-webkit-box-shadow:1px 3px 4px rgba(0,0,0,.09);box-shadow:1px 3px 4px rgba(0,0,0,.09);text-align:left}.search__outer::-webkit-scrollbar-track{border-radius:10px;background-color:#fcfcfc}.search__outer::-webkit-scrollbar{width:7px;height:7px;background-color:#fcfcfc}.search__outer::-webkit-scrollbar-thumb{border-radius:10px;background-color:#8f8f8f}.search__cross__img{width:15px;height:15px;margin:12px}.search__cross{position:absolute;top:0;right:0}.search__cross:hover{cursor:pointer}.search__outer__input{width:90%;height:30px;font-size:19px;outline:0;background-color:#fcfcfc;border:0;border-bottom:1px solid #757575;background-image:url(data:image/svg+xml;base64,PD94bWwgdmVyc2lvbj0iMS4wIiBlbmNvZGluZz0iaXNvLTg4NTktMSI/Pg0KPCEtLSBHZW5lcmF0b3I6IEFkb2JlIElsbHVzdHJhdG9yIDE5LjEuMCwgU1ZHIEV4cG9ydCBQbHVnLUluIC4gU1ZHIFZlcnNpb246IDYuMDAgQnVpbGQgMCkgIC0tPg0KPHN2ZyB2ZXJzaW9uPSIxLjEiIGlkPSJDYXBhXzEiIHhtbG5zPSJodHRwOi8vd3d3LnczLm9yZy8yMDAwL3N2ZyIgeG1sbnM6eGxpbms9Imh0dHA6Ly93d3cudzMub3JnLzE5OTkveGxpbmsiIHg9IjBweCIgeT0iMHB4Ig0KCSB2aWV3Qm94PSIwIDAgNDUxIDQ1MSIgc3R5bGU9ImVuYWJsZS1iYWNrZ3JvdW5kOm5ldyAwIDAgNDUxIDQ1MTsiIHhtbDpzcGFjZT0icHJlc2VydmUiPg0KPGc+DQoJPHBhdGggZD0iTTQ0Ny4wNSw0MjhsLTEwOS42LTEwOS42YzI5LjQtMzMuOCw0Ny4yLTc3LjksNDcuMi0xMjYuMUMzODQuNjUsODYuMiwyOTguMzUsMCwxOTIuMzUsMEM4Ni4yNSwwLDAuMDUsODYuMywwLjA1LDE5Mi4zDQoJCXM4Ni4zLDE5Mi4zLDE5Mi4zLDE5Mi4zYzQ4LjIsMCw5Mi4zLTE3LjgsMTI2LjEtNDcuMkw0MjguMDUsNDQ3YzIuNiwyLjYsNi4xLDQsOS41LDRzNi45LTEuMyw5LjUtNA0KCQlDNDUyLjI1LDQ0MS44LDQ1Mi4yNSw0MzMuMiw0NDcuMDUsNDI4eiBNMjYuOTUsMTkyLjNjMC05MS4yLDc0LjItMTY1LjMsMTY1LjMtMTY1LjNjOTEuMiwwLDE2NS4zLDc0LjIsMTY1LjMsMTY1LjMNCgkJcy03NC4xLDE2NS40LTE2NS4zLDE2NS40QzEwMS4xNSwzNTcuNywyNi45NSwyODMuNSwyNi45NSwxOTIuM3oiLz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjxnPg0KPC9nPg0KPGc+DQo8L2c+DQo8Zz4NCjwvZz4NCjwvc3ZnPg0K);background-repeat:no-repeat;background-position:left;background-size:15px;padding-left:25px}.search__outer__input:focus{outline:0}.search__outer .bar{position:relative;display:block;width:90%;margin-bottom:15px}.search__outer .bar:after,.search__outer .bar:before{content:"";height:2px;width:0;bottom:1px;position:absolute;background:#5264ae;-webkit-transition:.2s ease all;-o-transition:.2s ease all;transition:.2s ease all}.search__outer .bar:before{left:50%}.search__outer .bar:after{right:50%}.search__outer__input:focus~.bar:after,.search__outer__input:focus~.bar:before{width:50%}.search__result__single{margin-top:10px;padding:0 10px;border-bottom:1px solid #e6e6e6}.outer_div_page_results:hover,.search__result__box .active{background-color:#f5f5f5}.outer_div_page_results{margin:5px 0;overflow:auto;padding:3px 5px}.search__result__single a{text-decoration:none;cursor:pointer}.search__result__title{display:inline-block;font-weight:500;margin-bottom:15px;margin-top:0;font-size:15px;color:#6ea0ec;border-bottom:1px solid #6ea0ec}.search__result__subheading{color:#000;font-weight:700;float:left;width:20%;font-size:15px;margin-right:10px;overflow-x:hidden}.search__result__content{text-decoration:none;color:#000;font-size:15px;display:block;margin:0;line-height:inherit;float:right;width:calc(80% - 15px);text-align:left}.search__outer em{font-style:normal}.search__outer .search__result__title em{background-color:#e5f6ff;padding-bottom:3px;border-bottom-color:#000}.search__outer .search__result__content em{background-color:#e5f6ff;border-bottom:1px solid #000}.search__result__subheading em{border-bottom:1px solid #000}.br-for-hits{display:block;content:"";margin-top:10px}.rtd_ui_search_subtitle{all:unset;color:inherit;font-size:85%}@media (max-width:630px){.search__result__content,.search__result__subheading{float:none;width:90%}} \ No newline at end of file diff --git a/sphinx_search/_static/js/rtd_sphinx_search.js b/sphinx_search/_static/js/rtd_sphinx_search.js index 598bec1..cbfd3c7 100644 --- a/sphinx_search/_static/js/rtd_sphinx_search.js +++ b/sphinx_search/_static/js/rtd_sphinx_search.js @@ -1,12 +1,19 @@ -const MAX_SUGGESTIONS = 10; -let TOTAL_RESULTS = 0; +const MAX_SUGGESTIONS = 50; +const MAX_SECTION_RESULTS = 3; +const MAX_SUBSTRING_LIMIT = 100; + +let TOTAL_PAGE_RESULTS = 0; let SEARCH_QUERY = ""; +// this is used to store the total result counts, +// which includes all the sections and domains of all the pages. +let COUNT = 0; + /** * Debounce the function. * Usage: * - * var func = debounce(() => console.log("Hello World"), 3000); + * let func = debounce(() => console.log("Hello World"), 3000); * * // calling the func * func(); @@ -72,96 +79,296 @@ const createDomNode = (nodeName, attributes) => { }; /** - * Generate search suggestions list. - * Structure of the generated html which is - * returned from this function is :- + * Checks if data type is "string" or not * - * + * @param {*} data + * @return {Boolean} 'true' if type is "string" and length is > 0 + */ +const _is_string = str => { + if (typeof str === "string" && str.length > 0) { + return true; + } else { + return false; + } +}; + +/** + * Checks if data type is a non-empty array + * @param {*} data data whose type is to be checked + * @return {Boolean} returns true if data is non-empty array, else returns false + */ +const _is_array = arr => { + if (Array.isArray(arr) && arr.length > 0) { + return true; + } else { + return false; + } +}; + +/** + * Generate and return html structure + * for a page section result. * - * @param {Object} data response data from the search backend - * @param {String} projectName name (slug) of the project - * @return {Object} a
node with class "search__result__box" and with inner nodes + * @param {Object} sectionData object containing the result data + * @param {String} page_link link of the main page. It is used to construct the section link */ -const generateSuggestionsList = (data, projectName) => { - let search_result_box = createDomNode("div", { - class: "search__result__box" +const get_section_html = (sectionData, page_link) => { + let section_template = + ' \ +
\ + \ + <%= section_subheading %> \ + \ + <% for (var i = 0; i < section_content.length; ++i) { %> \ +

\ + <%= section_content[i] %> \ +

\ + <% } %>\ +
\ +
\ +
'; + + let section_subheading = sectionData._source.title; + let highlight = sectionData.highlight; + if (getHighlightListData(highlight, "sections.title")) { + section_subheading = getHighlightListData( + highlight, + "sections.title" + )[0]; + } + + let section_content = [ + sectionData._source.content.substring(0, MAX_SUBSTRING_LIMIT) + " ..." + ]; + + if (getHighlightListData(highlight, "sections.content")) { + let highlight_content = getHighlightListData( + highlight, + "sections.content" + ); + section_content = []; + for ( + let j = 0; + j < highlight_content.length && j < MAX_SECTION_RESULTS; + ++j + ) { + section_content.push("... " + highlight_content[j] + " ..."); + } + } + + let section_link = `${page_link}#${sectionData._source.id}`; + + let section_id = "hit__" + COUNT; + + let section_html = $u.template(section_template, { + section_link: section_link, + section_id: section_id, + section_subheading: section_subheading, + section_content: section_content }); - for (let i = 0; i < TOTAL_RESULTS; ++i) { - let search_result_single = createDomNode("div", { - class: "search__result__single", - id: "hit__" + (i + 1) - }); + return section_html; +}; - let link = createDomNode("a", { - href: data.results[i].link + DOCUMENTATION_OPTIONS.FILE_SUFFIX - }); +/** + * Returns value of the corresponding key (if present), + * else returns false. + * + * @param {Object} data object containing the data used for highlighting + * @param {String} key key whose values is to be returned + * @return {Array|Boolean} if key is present, it will return its value. Otherwise, return false + */ +const getHighlightListData = (data, key) => { + if (_is_array(data[key])) { + return data[key]; + } else { + return false; + } +}; - let content = createDomNode("div", { class: "content" }); +/** + * Generate and return html structure + * for a sphinx domain result. + * + * @param {Object} domainData object containing the result data + * @param {String} page_link link of the main page. It is used to construct the section link + */ +const get_domain_html = (domainData, page_link) => { + let domain_template = + ' \ +
\ + \ + <%= domain_subheading %> \ + \ +

<%= domain_content %>

\ +
\ +
\ +
'; + + let domain_link = `${page_link}#${domainData._source.anchor}`; + let domain_role_name = domainData._source.role_name; + let domain_type_display = domainData._source.type_display; + let domain_doc_display = domainData._source.doc_display; + let domain_display_name = domainData._source.display_name; + let domain_name = domainData._source.name; + + // take values from highlighted fields (if present) + if (domainData.highlight !== undefined && domainData.highlight !== null) { + let highlight = domainData.highlight; + + let name = getHighlightListData(highlight, "domains.name"); + let display_name = getHighlightListData( + highlight, + "domains.display_name" + ); + let type_display = getHighlightListData( + highlight, + "domains.type_display" + ); - let search_result_title = createDomNode("h2", { - class: "search__result__title" - }); - // use highlighted title (if present) - if (data.results[i].highlight.title !== undefined) { - search_result_title.innerHTML = data.results[i].highlight.title[0]; - } else { - search_result_title.innerHTML = data.results[i].title; + if (name) { + domain_name = name[0]; } - content.appendChild(search_result_title); - content.appendChild(createDomNode("br")); + if (display_name) { + domain_display_name = display_name[0]; + } - let search_result_path = createDomNode("small", { - class: "search__result__path" - }); - search_result_path.innerHTML = data.results[i].path; - - // check if the corresponding result is from same project or not. - // if it is not from same project, then it must be from a subproject. - // display the subproject. - if (data.results[i].project !== projectName) { - search_result_path.innerHTML = - data.results[i].path + - " (from " + - data.results[i].project + - ")"; + if (type_display) { + domain_type_display = type_display[0]; } + } - content.appendChild(search_result_path); + // preparing domain_content + let domain_content = ""; + if (_is_string(domain_type_display)) { + // domain_content = type_display -- + domain_content += domain_type_display + " -- "; + } + if (_is_string(domain_name)) { + // domain_content = type_display -- name + domain_content += domain_name + " "; + } + if (_is_string(domain_doc_display)) { + // domain_content = type_display -- name -- in doc_display + domain_content += "-- in " + domain_doc_display; + } - let search_result_content = createDomNode("p", { - class: "search__result__content" - }); - if (data.results[i].highlight.content !== undefined) { - search_result_content.innerHTML = - "... " + data.results[i].highlight.content + " ..."; - } else { - search_result_content.innerHTML = ""; + let domain_subheading = ""; + if (_is_string(domain_display_name)) { + // domain_subheading = (role_name) display_name + domain_subheading = "(" + domain_role_name + ") " + domain_display_name; + } else { + // domain_subheading = role_name + domain_subheading = domain_role_name; + } + + let domain_id = "hit__" + COUNT; + let domain_html = $u.template(domain_template, { + domain_link: domain_link, + domain_id: domain_id, + domain_content: domain_content, + domain_subheading: domain_subheading + }); + + return domain_html; +}; + +/** + * Generate search results for a single page. + * + * @param {Object} resultData search results of a page + * @return {Object} a
node with the results of a single page + */ +const generateSingleResult = (resultData, projectName) => { + let content = createDomNode("div"); + + let page_link_template = + ' \ +

\ + <%= page_title %> \ +

\ +
'; + + let page_link = `${resultData.link}${DOCUMENTATION_OPTIONS.FILE_SUFFIX}`; + let page_link_highlight = + page_link + "?highlight=" + encodeURIComponent(SEARCH_QUERY); + + let page_title = resultData.title; + + // if title is present in highlighted field, use that. + if (resultData.highlight !== undefined && resultData.highlight !== null) { + if ( + resultData.highlight.title !== undefined && + resultData.highlight.title !== null + ) { + page_title = resultData.highlight.title; } + } + + // if result is not from the same project, + // then it must be from subproject. + if (projectName !== resultData.project) { + page_title += + " " + + $u.template( + ' \ + (from project <%= project %>) \ + ', + { + project: resultData.project + } + ); + } + + page_title += "
"; + + content.innerHTML += $u.template(page_link_template, { + page_link: page_link_highlight, + page_title: page_title + }); + + for (let i = 0; i < resultData.inner_hits.length; ++i) { + const type = resultData.inner_hits[i].type; + COUNT += 1; + let html_structure = ""; + + if (type === "sections") { + html_structure = get_section_html( + resultData.inner_hits[i], + page_link + ); + } else if (type === "domains") { + html_structure = get_domain_html( + resultData.inner_hits[i], + page_link + ); + } + content.innerHTML += html_structure; + } + return content; +}; + +/** + * Generate search suggestions list. + * + * @param {Object} data response data from the search backend + * @param {String} projectName name (slug) of the project + * @return {Object} a
node with class "search__result__box" with results + */ +const generateSuggestionsList = (data, projectName) => { + let search_result_box = createDomNode("div", { + class: "search__result__box" + }); + + for (let i = 0; i < TOTAL_PAGE_RESULTS; ++i) { + let search_result_single = createDomNode("div", { + class: "search__result__single" + }); - content.appendChild(search_result_content); - link.appendChild(content); - search_result_single.appendChild(link); + let content = generateSingleResult(data.results[i], projectName); + + search_result_single.appendChild(content); search_result_box.appendChild(search_result_single); } return search_result_box; @@ -171,7 +378,7 @@ const generateSuggestionsList = (data, projectName) => { * Removes .active class from all the suggestions. */ const removeAllActive = () => { - const results = document.querySelectorAll(".search__result__single"); + const results = document.querySelectorAll(".outer_div_page_results"); const results_arr = Object.keys(results).map(i => results[i]); for (let i = 1; i <= results_arr.length; ++i) { results_arr[i - 1].classList.remove("active"); @@ -206,7 +413,21 @@ const addActive = current_focus => { * @return {Object} Input field node */ const getInputField = () => { - const inputField = document.querySelector("div[role='search'] input"); + let inputField; + + // on search some pages (like search.html), + // no div is present with role="search", + // in that case, use the other query to select + // the input field + try { + inputField = document.querySelector("div[role='search'] input"); + if (inputField === undefined || inputField === null) { + throw "'div[role='search'] input' not found"; + } + } catch (err) { + inputField = document.querySelector("input[name='q']"); + } + return inputField; }; @@ -230,7 +451,7 @@ const removeResults = () => { const getErrorDiv = err_msg => { let err_div = createDomNode("div", { class: "search__result__box", - style: "color: black; min-width: 300px" + style: "color: black; min-width: 300px; font-weight: 700" }); err_div.innerHTML = err_msg; return err_div; @@ -252,7 +473,7 @@ const fetchAndGenerateResults = (search_url, projectName) => { // the user. removeResults(); let search_loding = createDomNode("div", { class: "search__result__box" }); - search_loding.innerHTML = "Searching ...."; + search_loding.innerHTML = "Searching ...."; search_outer.appendChild(search_loding); let ajaxFunc = () => { @@ -268,7 +489,7 @@ const fetchAndGenerateResults = (search_url, projectName) => { typeof resp.responseJSON !== "undefined" ) { if (resp.responseJSON.results.length > 0) { - TOTAL_RESULTS = + TOTAL_PAGE_RESULTS = MAX_SUGGESTIONS <= resp.responseJSON.results.length ? MAX_SUGGESTIONS : resp.responseJSON.results.length; @@ -287,14 +508,14 @@ const fetchAndGenerateResults = (search_url, projectName) => { }); } else { removeResults(); - var err_div = getErrorDiv("No Results Found"); + let err_div = getErrorDiv("No Results Found"); search_outer.appendChild(err_div); } } }, error: (resp, status_code, error) => { removeResults(); - var err_div = getErrorDiv("Error Occurred. Please try again."); + let err_div = getErrorDiv("Error Occurred. Please try again."); search_outer.appendChild(err_div); } }); @@ -308,56 +529,25 @@ const fetchAndGenerateResults = (search_url, projectName) => { * appended to the as soon as the page loads. * This html structure will serve as the boilerplate * to show our search results. - * It generates the following html structure :- - * - *
- *
- *
- * - * - * - * - *
- * - * - *
- *
* - * @return {Object} object containing the nodes with classes "search__outer__wrapper", "search__outer__input" and "search__outer" + * @return {String} initial html structure */ const generateAndReturnInitialHtml = () => { - let search_outer_wrapper = createDomNode("div", { - class: "search__outer__wrapper search__backdrop" - }); - - let search_outer = createDomNode("div", { class: "search__outer" }); - - let cross_icon = createDomNode("div", { - class: "search__cross", - title: "Close" - }); - cross_icon.innerHTML = - ""; - search_outer.appendChild(cross_icon); - - let search_outer_input = createDomNode("input", { - class: "search__outer__input", - placeholder: "Search ..." - }); - - // for material ui design input field - let horizontal_bar = createDomNode("span", { class: "bar" }); - - search_outer.appendChild(search_outer_input); - search_outer.appendChild(horizontal_bar); - search_outer_wrapper.appendChild(search_outer); - - return { - search_outer_wrapper, - search_outer_input, - search_outer, - cross_icon - }; + let initialHtml = + '
\ +
\ +
\ + \ + \ + \ + \ +
\ + \ + \ +
\ +
'; + + return initialHtml; }; /** @@ -410,12 +600,12 @@ window.addEventListener("DOMContentLoaded", evt => { const language = READTHEDOCS_DATA.language || "en"; const api_host = READTHEDOCS_DATA.api_host; - const initialHtml = generateAndReturnInitialHtml(); - let search_outer_wrapper = initialHtml.search_outer_wrapper; - let search_outer_input = initialHtml.search_outer_input; - let cross_icon = initialHtml.cross_icon; + let initialHtml = generateAndReturnInitialHtml(); + document.body.innerHTML += initialHtml; - document.body.appendChild(search_outer_wrapper); + let search_outer_wrapper = document.querySelector('.search__outer__wrapper'); + let search_outer_input = document.querySelector('.search__outer__input'); + let cross_icon = document.querySelector('.search__cross'); // this denotes the search suggestion which is currently selected // via tha ArrowUp/ArrowDown keys. @@ -431,6 +621,8 @@ window.addEventListener("DOMContentLoaded", evt => { search_outer_input.addEventListener("input", e => { SEARCH_QUERY = e.target.value; + COUNT = 0; + let search_params = { q: encodeURIComponent(SEARCH_QUERY), project: project, @@ -451,7 +643,11 @@ window.addEventListener("DOMContentLoaded", evt => { current_request = fetchAndGenerateResults(search_url, project); current_request(); } else { - removeResults(); + // if the last request returns the results, + // the suggestions list is generated even if there + // is no query. To prevent that, this function + // is debounced here. + debounce(removeResults, 600)(); } }); @@ -460,7 +656,7 @@ window.addEventListener("DOMContentLoaded", evt => { if (e.keyCode === 40) { e.preventDefault(); current_focus += 1; - if (current_focus > TOTAL_RESULTS) { + if (current_focus > COUNT) { current_focus = 1; } removeAllActive(); @@ -472,7 +668,7 @@ window.addEventListener("DOMContentLoaded", evt => { e.preventDefault(); current_focus -= 1; if (current_focus < 1) { - current_focus = TOTAL_RESULTS; + current_focus = COUNT; } removeAllActive(); addActive(current_focus); @@ -482,19 +678,19 @@ window.addEventListener("DOMContentLoaded", evt => { if (e.keyCode === 13) { e.preventDefault(); const current_item = document.querySelector( - ".search__result__single.active" + ".outer_div_page_results.active" ); // if an item is selected, // then redirect to its link if (current_item !== null) { - const link = current_item.firstChild["href"]; + const link = current_item.parentElement["href"]; window.location.href = link; } else { // submit search form if there // is no active item. - const form = document.querySelector( - "div[role='search'] form" - ); + const input_field = getInputField(); + const form = input_field.parentElement; + search_bar.value = SEARCH_QUERY || ""; form.submit(); } diff --git a/sphinx_search/_static/js/rtd_sphinx_search.min.js b/sphinx_search/_static/js/rtd_sphinx_search.min.js index 03bb48e..281f1bb 100644 --- a/sphinx_search/_static/js/rtd_sphinx_search.min.js +++ b/sphinx_search/_static/js/rtd_sphinx_search.min.js @@ -1 +1 @@ -"use strict";var MAX_SUGGESTIONS=10,TOTAL_RESULTS=0,SEARCH_QUERY="",debounce=function(t,n){function e(){var e=this,r=arguments;clearTimeout(o),o=setTimeout(function(){return t.apply(e,r)},n)}var o;return e.cancel=function(){clearTimeout(o),o=null},e},convertObjToUrlParams=function(r){return Object.keys(r).map(function(e){return e+"="+r[e]}).join("&")},createDomNode=function(e,r){var t=document.createElement(e);for(var n in r)t.setAttribute(n,r[n]);return t},generateSuggestionsList=function(e,r){for(var t=createDomNode("div",{class:"search__result__box"}),n=0;n"+e.results[n].project+")"),a.appendChild(l);var i=createDomNode("p",{class:"search__result__content"});void 0!==e.results[n].highlight.content?i.innerHTML="... "+e.results[n].highlight.content+" ...":i.innerHTML="",a.appendChild(i),s.appendChild(a),o.appendChild(s),t.appendChild(o)}return t},removeAllActive=function(){for(var r=document.querySelectorAll(".search__result__single"),e=Object.keys(r).map(function(e){return r[e]}),t=1;t<=e.length;++t)e[t-1].classList.remove("active")},addActive=function(e){var r=document.querySelector("#hit__"+e);null!==r&&(r.classList.add("active"),r.scrollIntoView({behavior:"smooth",block:"nearest",inline:"start"}))},getInputField=function(){return document.querySelector("div[role='search'] input")},removeResults=function(){for(var e=document.querySelectorAll(".search__result__box"),r=0;r",r.appendChild(t);var n=createDomNode("input",{class:"search__outer__input",placeholder:"Search ..."}),o=createDomNode("span",{class:"bar"});return r.appendChild(n),r.appendChild(o),e.appendChild(r),{search_outer_wrapper:e,search_outer_input:n,search_outer:r,cross_icon:t}},showSearchModal=function(){removeResults(),getInputField().blur(),$(".search__outer__wrapper").fadeIn(400,function(){var e=document.querySelector(".search__outer__input");null!==e&&(e.value="",e.focus())})},removeSearchModal=function(){removeResults();var e=document.querySelector(".search__outer__input");null!==e&&(e.value="",e.blur()),$(".search__outer__wrapper").fadeOut(400)};window.addEventListener("DOMContentLoaded",function(e){if(void 0!==READTHEDOCS_DATA){var n=READTHEDOCS_DATA.project,o=READTHEDOCS_DATA.version,s=READTHEDOCS_DATA.language||"en",a=READTHEDOCS_DATA.api_host,r=generateAndReturnInitialHtml(),t=r.search_outer_wrapper,c=r.search_outer_input,l=r.cross_icon;document.body.appendChild(t);var i=0,u=null,d=getInputField();d.addEventListener("focus",function(e){showSearchModal()}),c.addEventListener("input",function(e){SEARCH_QUERY=e.target.value;var r={q:encodeURIComponent(SEARCH_QUERY),project:n,version:o,language:s},t=a+"/api/v2/docsearch/?"+convertObjToUrlParams(r);"string"==typeof SEARCH_QUERY&&0
<%= section_subheading %> <% for (var i = 0; i < section_content.length; ++i) { %>

<%= section_content[i] %>

<% } %>

',{section_link:a,section_id:c,section_subheading:n,section_content:i})},getHighlightListData=function(e,t){return!!_is_array(e[t])&&e[t]},get_domain_html=function(e,t){var n="".concat(t,"#").concat(e._source.anchor),r=e._source.role_name,i=e._source.type_display,o=e._source.doc_display,s=e._source.display_name,a=e._source.name;if(void 0!==e.highlight&&null!==e.highlight){var c=e.highlight,l=getHighlightListData(c,"domains.name"),u=getHighlightListData(c,"domains.display_name"),_=getHighlightListData(c,"domains.type_display");l&&(a=l[0]),u&&(s=u[0]),_&&(i=_[0])}var d="";_is_string(i)&&(d+=i+" -- "),_is_string(a)&&(d+=a+" "),_is_string(o)&&(d+="-- in "+o);var h="";h=_is_string(s)?"("+r+") "+s:r;var v="hit__"+COUNT;return $u.template('
<%= domain_subheading %>

<%= domain_content %>


',{domain_link:n,domain_id:v,domain_content:d,domain_subheading:h})},generateSingleResult=function(e,t){var n=createDomNode("div"),r="".concat(e.link).concat(DOCUMENTATION_OPTIONS.FILE_SUFFIX),i=r+"?highlight="+encodeURIComponent(SEARCH_QUERY),o=e.title;void 0!==e.highlight&&null!==e.highlight&&void 0!==e.highlight.title&&null!==e.highlight.title&&(o=e.highlight.title),t!==e.project&&(o+=" "+$u.template(' (from project <%= project %>) ',{project:e.project})),o+="
",n.innerHTML+=$u.template('

<%= page_title %>

',{page_link:i,page_title:o});for(var s=0;sSearching ....",o.appendChild(t);var n=function(){$.ajax({url:e,crossDomain:!0,xhrFields:{withCredentials:!0},complete:function(e,t){if("success"===t||void 0!==e.responseJSON)if(0
\x3c!--?xml version="1.0" encoding="UTF-8"?--\x3e
'},showSearchModal=function(){removeResults(),getInputField().blur(),$(".search__outer__wrapper").fadeIn(400,function(){var e=document.querySelector(".search__outer__input");null!==e&&(e.value="",e.focus())})},removeSearchModal=function(){removeResults();var e=document.querySelector(".search__outer__input");null!==e&&(e.value="",e.blur()),$(".search__outer__wrapper").fadeOut(400)};window.addEventListener("DOMContentLoaded",function(e){if(void 0!==READTHEDOCS_DATA){var r=READTHEDOCS_DATA.project,i=READTHEDOCS_DATA.version,o=READTHEDOCS_DATA.language||"en",s=READTHEDOCS_DATA.api_host,t=generateAndReturnInitialHtml();document.body.innerHTML+=t;var n=document.querySelector(".search__outer__wrapper"),a=document.querySelector(".search__outer__input"),c=document.querySelector(".search__cross"),l=0,u=null,_=getInputField();_.addEventListener("focus",function(e){showSearchModal()}),a.addEventListener("input",function(e){SEARCH_QUERY=e.target.value,COUNT=0;var t={q:encodeURIComponent(SEARCH_QUERY),project:r,version:i,language:o},n=s+"/api/v2/docsearch/?"+convertObjToUrlParams(t);"string"==typeof SEARCH_QUERY&&0Sphinx", - "Using Markdown with Sphinx" - ], - "title": ["Getting Started with Sphinx"], - "content": [ - "Getting Started with Sphinx. Sphinx is a powerful documentation generator that has many great features", - "docs. Run sphinx-quickstart in there:. cd docs sphinx-quickstart. This quick start will walk you through", - "Using Markdown with Sphinx. You can use Markdown and reStructuredText in the same Sphinx project." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Adding Custom CSS or JavaScript to a Sphinx Project", - "path": "guides/adding-custom-css", - "link": "https://docs.readthedocs.io/en/latest/guides/adding-custom-css", - "highlight": { - "headers": [ - "Adding Custom CSS or JavaScript to a Sphinx Project" - ], "title": [ - "Adding Custom CSS or JavaScript to a Sphinx Project" - ], - "content": [ - "Adding Custom CSS or JavaScript to a Sphinx Project. The easiest way to add custom stylesheets or scripts", - "way that allows for overriding of existing styles or scripts, is to add these files using a conf.py Sphinx", - "Inside your conf.py, if a function setup(app) exists, Sphinx will call this function as a normal extension" - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Read the Docs data passed to Sphinx build context", - "path": "design/theme-context", - "link": "https://docs.readthedocs.io/en/latest/design/theme-context", - "highlight": { - "headers": [ - "Read the Docs data passed to Sphinx build context" - ], - "title": [ - "Read the Docs data passed to Sphinx build context" - ], - "content": [ - "Read the Docs data passed to Sphinx build context. Before calling sphinx-build to render your docs, Read", - "the Docs injects some extra context in the templates by using the html_context Sphinx setting in the", - "More information about Sphinx variables can be found in the Sphinx documentation." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Customizing Advertising", - "path": "advertising/ad-customization", - "link": "https://docs.readthedocs.io/en/latest/advertising/ad-customization", - "highlight": { - "headers": ["In Sphinx"], - "content": [ - "<div id="ethical-ad-placement"></div>. In Sphinx. In Sphinx, this is typically done by adding a new template" - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Embed API", - "path": "embed", - "link": "https://docs.readthedocs.io/en/latest/embed", - "highlight": { - "headers": ["Sphinx Extension"], - "content": [ - "How to use it. Sphinx Extension. You can embed content directly in Sphinx with builds on Read the Docs." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "User-defined Redirects", - "path": "user-defined-redirects", - "link": "https://docs.readthedocs.io/en/latest/user-defined-redirects", - "highlight": { - "headers": ["Sphinx Redirects"], - "content": [ - "Sphinx Redirects. We also support redirects for changing the type of documentation Sphinx is building." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Build Process", - "path": "builds", - "link": "https://docs.readthedocs.io/en/latest/builds", - "highlight": { - "headers": ["Sphinx"], - "content": [ - "Sphinx. When you choose Sphinx as your Documentation Type, we will first look for a conf.py file in your", - "Then Sphinx will build any files with an .rst extension.", - "When we build your Sphinx documentation, we run sphinx-build -b html ." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Configuration File V2", - "path": "config-file/v2", - "link": "https://docs.readthedocs.io/en/latest/config-file/v2", - "highlight": { - "headers": ["sphinx"], - "content": [ - "sphinx: configuration: docs/conf.py # Build documentation with MkDocs #mkdocs: # configuration: mkdocs.yml", - "Configuration for Sphinx documentation (this is the default documentation type).", - "type for the Sphinx documentation." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Google Summer of Code", - "path": "gsoc", - "link": "https://docs.readthedocs.io/en/latest/gsoc", - "highlight": { - "headers": ["Build a new Sphinx theme"], - "content": [ - "Integration with OpenAPI/Swagger. Integrate the existing tooling around OpenAPI & Swagger into Sphinx", - "theme. Sphinx v2 will introduce a new format for themes, supporting HTML5 and new markup.", - "We are hoping to buid a new Sphinx theme that supports this new structure." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Frequently Asked Questions", - "path": "faq", - "link": "https://docs.readthedocs.io/en/latest/faq", - "highlight": { - "headers": [ - "I want to use the Blue/Default Sphinx theme", - "Sphinx" - ], - "content": [ - "With Sphinx you can use the built-in autodoc_mock_imports for mocking.", - "Where it finds the conf.py, it will run sphinx-build in that directory.", - "Sphinx. Sphinx uses html_extra option to add static files to the output." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Manage Translations", - "path": "guides/manage-translations", - "link": "https://docs.readthedocs.io/en/latest/guides/manage-translations", - "highlight": { - "content": [ - "Translate text from source language. Manually. We recommend using sphinx-intl tool for this workflow.", - ""It supports :ref:`Sphinx <sphinx>` docs written with reStructuredText." msgstr "" "FILL HERE BY TARGET", - "LANGUAGE FILL HERE BY TARGET LANGUAGE FILL HERE " "BY TARGET LANGUAGE :ref:`Sphinx <sphinx>` FILL HERE" - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Build PDF format for non-ASCII languages", - "path": "guides/pdf-non-ascii-languages", - "link": "https://docs.readthedocs.io/en/latest/guides/pdf-non-ascii-languages", - "highlight": { - "content": [ - "Build PDF format for non-ASCII languages. Sphinx offers different LaTeX engines that support Unicode characters", - "By default Sphinx uses pdflatex, which does not have good support for Unicode characters and may make", - "There are several settings that can be defined (all the ones starting with latex_), to modify Sphinx" - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Conda Support", - "path": "conda", - "link": "https://docs.readthedocs.io/en/latest/conda", - "highlight": { - "content": [ - "This Conda environment will also have Sphinx and other build time dependencies installed.", - "operations that we support currently:. Environment Creation (conda env create). Dependency Installation (Sphinx" - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Local VM Install", - "path": "custom_installs/local_rtd_vm", - "link": "https://docs.readthedocs.io/en/latest/custom_installs/local_rtd_vm", - "highlight": { - "content": [ - "/docs/source. Sphinx:. sudo pip install sphinx. Note. Not using sudo may prevent access.", - "Run the following Sphinx commands:. make html. This generates the HTML documentation site using the default", - "Sphinx theme." - ] - } - }, - { - "project": "docs", - "version": "latest", - "title": "Read the Docs: Documentation Simplified", - "path": "index", - "link": "https://docs.readthedocs.io/en/latest/index", - "highlight": { - "content": [ - "Learn about documentation authoring tools such as Sphinx and MkDocs to help you create fantastic documentation", - "I want to use the Blue/Default Sphinx theme. I want to use the Read the Docs theme locally. Image scaling", - "Configuration File. Version 2. Version 1. Guides. Adding Custom CSS or JavaScript to a Sphinx Project. Enabling" - ] - } + "Developing a “Hello world” extension" + ] + }, + "inner_hits": [ + { + "type": "sections", + "_source": { + "id": "overview", + "title": "Overview", + "content": "We want the extension to add the following to Sphinx:\nA helloworld directive, that will simply output the text “hello world”." + }, + "highlight": { + "sections.content": ["world”."] + } + }, + { + "type": "sections", + "_source": { + "id": "developing-a-hello-world-extension", + "title": "Developing a “Hello world” extension", + "content": "Only basic information is provided in this tutorial. For more information, refer to the other tutorials that go into more details.\n\nWarning\nFor this extension, you will need some basic understanding of docutils and Python.\n" + }, + "highlight": { + "sections.title": [ + "Developing a “Hello world” extension" + ] + } + }, + { + "type": "sections", + "_source": { + "id": "using-the-extension", + "title": "Using the extension", + "content": "The extension has to be declared in your conf.py file to make Sphinx aware of it. There are two steps necessary here:\nAdd the _ext directory to the Python path using sys.path.append. This should be placed at the top of the file.\nUpdate or create the extensions list and add the extension file name to the list\nFor example:\nimport os import sys sys.path.append(os.path.abspath(\"./_ext\")) extensions = ['helloworld']\nTip\nWe’re not distributing this extension as a Python package, we need to modify the Python path so Sphinx can find our extension. This is why we need the call to sys.path.append.\nYou can now use the extension in a file. For example:\nSome intro text here... .. helloworld:: Some more text here...\nThe sample above would generate:\nSome intro text here... Hello World! Some more text here..." + }, + "highlight": { + "sections.content": [ + "Hello World! Some more text here..." + ] + } + } + ] } ] } diff --git a/tests/test_extension.py b/tests/test_extension.py index fe47730..97cc903 100644 --- a/tests/test_extension.py +++ b/tests/test_extension.py @@ -4,7 +4,9 @@ import os import pytest + from tests import TEST_DOCS_SRC +from sphinx_search.extension import ASSETS_FILES @pytest.mark.sphinx(srcdir=TEST_DOCS_SRC) @@ -13,13 +15,62 @@ def test_static_files_exists(app, status, warning): app.build() path = app.outdir - js_file = os.path.join(path, '_static', 'js', 'rtd_sphinx_search.min.js') - css_file = os.path.join(path, '_static', 'css', 'rtd_sphinx_search.min.css') + static_files = ASSETS_FILES['minified'] + ASSETS_FILES['un-minified'] + + for file in static_files: + file_path = os.path.join(path, '_static', file) + assert ( + os.path.exists(file_path) + ), f'{file_path} should be present in the _build folder' + + +@pytest.mark.sphinx( + srcdir=TEST_DOCS_SRC, + confoverrides={ + 'rtd_sphinx_search_file_type': 'minified' + } +) +def test_minified_static_files_injected_in_html(selenium, app, status, warning): + """Test if the static files are correctly injected in the html.""" + app.build() + path = app.outdir / 'index.html' + + selenium.get(f'file://{path}') + page_source = selenium.page_source + + assert app.config.rtd_sphinx_search_file_type == 'minified' + + file_type = app.config.rtd_sphinx_search_file_type + files = ASSETS_FILES[file_type] + + for file in files: + file_name = file.split('/')[-1] + assert ( + page_source.count(file_name) == 1 + ), f'{file} should be present in the page source' + + +@pytest.mark.sphinx( + srcdir=TEST_DOCS_SRC, + confoverrides={ + 'rtd_sphinx_search_file_type': 'un-minified' + } +) +def test_un_minified_static_files_injected_in_html(selenium, app, status, warning): + """Test if the static files are correctly injected in the html.""" + app.build() + path = app.outdir / 'index.html' + + selenium.get(f'file://{path}') + page_source = selenium.page_source + + assert app.config.rtd_sphinx_search_file_type == 'un-minified' - assert ( - os.path.exists(js_file) is True - ), 'js file should be copied to build folder' + file_type = app.config.rtd_sphinx_search_file_type + files = ASSETS_FILES[file_type] - assert ( - os.path.exists(css_file) is True - ), 'css file should be copied to build folder' + for file in files: + file_name = file.split('/')[-1] + assert ( + page_source.count(file_name) == 1 + ), f'{file_name} should be present in the page source' diff --git a/tests/test_ui.py b/tests/test_ui.py index 30c5339..6d7cc97 100644 --- a/tests/test_ui.py +++ b/tests/test_ui.py @@ -98,7 +98,7 @@ def test_appending_of_initial_html(selenium, app, status, warning):
- + @@ -108,12 +108,15 @@ def test_appending_of_initial_html(selenium, app, status, warning):
''' - # removing all whitespaces and newlines between html tags + initial_html = [ele.strip() for ele in initial_html.split('\n') if ele] - assert ( - ''.join(initial_html) in selenium.page_source - ), 'initial html must be present when the page finished loading.' + for line in initial_html: + if line: + assert ( + line in selenium.page_source + ), f'{line} -- must be present in page source' + @pytest.mark.sphinx(srcdir=TEST_DOCS_SRC) def test_opening_of_search_modal(selenium, app, status, warning): @@ -376,9 +379,6 @@ def test_searching_msg(selenium, app, status, warning): assert ( search_result_box.text == 'Searching ....' ), 'user should be notified that search is in progress' - assert ( - len(search_result_box.find_elements_by_css_selector('*')) == 0 - ), 'search result box should not have any child elements because there are no results' WebDriverWait(selenium, 10).until( EC.text_to_be_present_in_element( @@ -387,11 +387,14 @@ def test_searching_msg(selenium, app, status, warning): ) ) - # fetching search_result_box again to update its content + # fetching it again from the DOM to update its status search_result_box = selenium.find_element_by_class_name( 'search__result__box' ) + assert ( + len(search_result_box.find_elements_by_css_selector('*')) == 0 + ), 'search result box should not have any child elements because there are no results' assert ( search_result_box.text == 'No Results Found' ), 'user should be notified that there are no results' @@ -449,8 +452,16 @@ def test_results_displayed_to_user(selenium, app, status, warning): 'search__result__single' ) ) - == 10 - ), 'search result box should have maximum 10 results' + == 1 + ), 'search result box should have results from only 1 page (as per the dummy_results.json)' + + assert ( + len( + search_result_box.find_elements_by_class_name( + 'outer_div_page_results' + ) + ) == 3 + ), 'total 3 results should be shown to the user (as per the dummy_results.json)' @pytest.mark.sphinx(srcdir=TEST_DOCS_SRC) @@ -499,28 +510,28 @@ def test_navigate_results_with_arrow_up_and_down(selenium, app, status, warning) 'search__result__box' ) results = selenium.find_elements_by_class_name( - 'search__result__single' + 'outer_div_page_results' ) search_outer_input.send_keys(Keys.DOWN) assert results[0] == selenium.find_element_by_css_selector( - '.search__result__single.active' + '.outer_div_page_results.active' ), 'first result should be active' search_outer_input.send_keys(Keys.DOWN) assert results[1] == selenium.find_element_by_css_selector( - '.search__result__single.active' + '.outer_div_page_results.active' ), 'second result should be active' search_outer_input.send_keys(Keys.UP) search_outer_input.send_keys(Keys.UP) assert results[-1] == selenium.find_element_by_css_selector( - '.search__result__single.active' + '.outer_div_page_results.active' ), 'last result should be active' search_outer_input.send_keys(Keys.DOWN) assert results[0] == selenium.find_element_by_css_selector( - '.search__result__single.active' + '.outer_div_page_results.active' ), 'first result should be active' diff --git a/tox.ini b/tox.ini index 868a1b8..1156392 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 3.10 envlist = - py{36}-sphinx{18} + py{36,37}-sphinx{18} py{36,37}-sphinx{20,21} docs skipsdist = True @@ -13,7 +13,7 @@ deps = pytest-selenium sphinx18: Sphinx<1.9 sphinx20: Sphinx<2.1 - sphinx21: Sphinx==2.1.0 + sphinx21: Sphinx<2.2 commands = pytest {posargs} [testenv:docs]