diff --git a/CHANGES.rst b/CHANGES.rst index 86e59279491..0f9e5c2c736 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -26,6 +26,9 @@ Bugs fixed * LaTeX: fix a ``7.4.0`` typo in a default for ``\sphinxboxsetup`` (refs: PR #13152). Patch by Jean-François B. +* #13097: HTML Search: serialize search index in JSON format, to + handle a query edge-case. + Patch by James Addison Testing ------- diff --git a/sphinx/builders/html/__init__.py b/sphinx/builders/html/__init__.py index 9a1001fceaf..d2d57e2b645 100644 --- a/sphinx/builders/html/__init__.py +++ b/sphinx/builders/html/__init__.py @@ -124,7 +124,7 @@ class StandaloneHTMLBuilder(Builder): supported_image_types = ['image/svg+xml', 'image/png', 'image/gif', 'image/jpeg'] supported_remote_images = True supported_data_uri_images = True - searchindex_filename = 'searchindex.js' + searchindex_filename = 'searchindex.json' add_permalinks = True allow_sharp_as_current_path = True embedded = False # for things like HTML help or Qt help: suppresses sidebar diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 3f19d3663a0..abcb485ad9d 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -164,19 +164,14 @@ class _JavaScriptIndex: on the documentation search object to register the index. """ - PREFIX = 'Search.setIndex(' - SUFFIX = ')' - def dumps(self, data: Any) -> str: - data_json = json.dumps(data, separators=(',', ':'), sort_keys=True) - return self.PREFIX + data_json + self.SUFFIX + return json.dumps(data, separators=(',', ':'), sort_keys=True) def loads(self, s: str) -> Any: - data = s[len(self.PREFIX) : -len(self.SUFFIX)] - if not data or not s.startswith(self.PREFIX) or not s.endswith(self.SUFFIX): + if not s: msg = 'invalid data' raise ValueError(msg) - return json.loads(data) + return json.loads(s) def dump(self, data: Any, f: IO[str]) -> None: f.write(self.dumps(data)) diff --git a/sphinx/themes/basic/search.html b/sphinx/themes/basic/search.html index 0ce54c43424..59730288ca6 100644 --- a/sphinx/themes/basic/search.html +++ b/sphinx/themes/basic/search.html @@ -7,7 +7,7 @@ {%- endblock %} {% block extrahead %} - + {{ super() }} {% endblock %} diff --git a/sphinx/themes/basic/static/searchindex.js b/sphinx/themes/basic/static/searchindex.js new file mode 100644 index 00000000000..c7cd483aaf7 --- /dev/null +++ b/sphinx/themes/basic/static/searchindex.js @@ -0,0 +1 @@ +window.fetch("searchindex.json").then(response => response.json()).then(Search.setIndex); diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index aaf078d2b91..f13f5027934 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -514,8 +514,8 @@ const Search = { searchTerms.forEach((word) => { const files = []; const arr = [ - { files: terms[word], score: Scorer.term }, - { files: titleTerms[word], score: Scorer.title }, + { files: terms.hasOwnProperty(word) ? terms[word] : [], score: Scorer.term }, + { files: titleTerms.hasOwnProperty(word) ? titleTerms[word] : [], score: Scorer.title }, ]; // add support for partial matches if (word.length > 2) { diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js deleted file mode 100644 index e5837e65d56..00000000000 --- a/tests/js/fixtures/cpp/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({"alltitles":{},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{"sphinx (c++ class)":[[0,"_CPPv46Sphinx",false]]},"objects":{"":[[0,0,1,"_CPPv46Sphinx","Sphinx"]]},"objnames":{"0":["cpp","class","C++ class"]},"objtypes":{"0":"cpp:class"},"terms":{"The":0,"becaus":0,"c":0,"can":0,"cardin":0,"challeng":0,"charact":0,"class":0,"descript":0,"drop":0,"engin":0,"fixtur":0,"frequent":0,"gener":0,"i":0,"index":0,"inflat":0,"mathemat":0,"occur":0,"often":0,"project":0,"punctuat":0,"queri":0,"relat":0,"sampl":0,"search":0,"size":0,"sphinx":0,"term":0,"thei":0,"thi":0,"token":0,"us":0,"web":0,"would":0},"titles":["<no title>"],"titleterms":{}}) \ No newline at end of file diff --git a/tests/js/fixtures/cpp/searchindex.json b/tests/js/fixtures/cpp/searchindex.json new file mode 100644 index 00000000000..188056c9fcf --- /dev/null +++ b/tests/js/fixtures/cpp/searchindex.json @@ -0,0 +1 @@ +{"alltitles":{},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{"sphinx (c++ class)":[[0,"_CPPv46Sphinx",false]]},"objects":{"":[[0,0,1,"_CPPv46Sphinx","Sphinx"]]},"objnames":{"0":["cpp","class","C++ class"]},"objtypes":{"0":"cpp:class"},"terms":{"The":0,"becaus":0,"c":0,"can":0,"cardin":0,"challeng":0,"charact":0,"class":0,"descript":0,"drop":0,"engin":0,"fixtur":0,"frequent":0,"gener":0,"i":0,"index":0,"inflat":0,"mathemat":0,"occur":0,"often":0,"project":0,"punctuat":0,"queri":0,"relat":0,"sampl":0,"search":0,"size":0,"sphinx":0,"term":0,"thei":0,"thi":0,"token":0,"us":0,"web":0,"would":0},"titles":["<no title>"],"titleterms":{}} \ No newline at end of file diff --git a/tests/js/fixtures/ecmascript/searchindex.json b/tests/js/fixtures/ecmascript/searchindex.json new file mode 100644 index 00000000000..aef1ae4e0fa --- /dev/null +++ b/tests/js/fixtures/ecmascript/searchindex.json @@ -0,0 +1 @@ +{"alltitles":{"ECMAScript":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"__proto__":0,"access":0,"aka":0,"an":0,"ani":0,"engin":0,"fixtur":0,"gener":0,"i":0,"index":0,"instanc":0,"javascript":0,"object":0,"project":0,"properti":0,"prototyp":0,"sampl":0,"search":0,"thi":0,"us":0},"titles":["ECMAScript"],"titleterms":{"ecmascript":0}} \ No newline at end of file diff --git a/tests/js/fixtures/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js deleted file mode 100644 index b3e2977792c..00000000000 --- a/tests/js/fixtures/multiterm/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({"alltitles":{"Main Page":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"At":0,"adjac":0,"all":0,"an":0,"appear":0,"applic":0,"ar":0,"built":0,"can":0,"check":0,"contain":0,"do":0,"document":0,"doesn":0,"each":0,"fixtur":0,"format":0,"function":0,"futur":0,"html":0,"i":0,"includ":0,"match":0,"messag":0,"multipl":0,"multiterm":0,"order":0,"other":0,"output":0,"perform":0,"perhap":0,"phrase":0,"project":0,"queri":0,"requir":0,"same":0,"search":0,"successfulli":0,"support":0,"t":0,"term":0,"test":0,"thi":0,"time":0,"us":0,"when":0,"write":0},"titles":["Main Page"],"titleterms":{"main":0,"page":0}}) \ No newline at end of file diff --git a/tests/js/fixtures/multiterm/searchindex.json b/tests/js/fixtures/multiterm/searchindex.json new file mode 100644 index 00000000000..e14c6d48e60 --- /dev/null +++ b/tests/js/fixtures/multiterm/searchindex.json @@ -0,0 +1 @@ +{"alltitles":{"Main Page":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"At":0,"adjac":0,"all":0,"an":0,"appear":0,"applic":0,"ar":0,"built":0,"can":0,"check":0,"contain":0,"do":0,"document":0,"doesn":0,"each":0,"fixtur":0,"format":0,"function":0,"futur":0,"html":0,"i":0,"includ":0,"match":0,"messag":0,"multipl":0,"multiterm":0,"order":0,"other":0,"output":0,"perform":0,"perhap":0,"phrase":0,"project":0,"queri":0,"requir":0,"same":0,"search":0,"successfulli":0,"support":0,"t":0,"term":0,"test":0,"thi":0,"time":0,"us":0,"when":0,"write":0},"titles":["Main Page"],"titleterms":{"main":0,"page":0}} \ No newline at end of file diff --git a/tests/js/fixtures/partial/searchindex.js b/tests/js/fixtures/partial/searchindex.js deleted file mode 100644 index ac024bf0c6e..00000000000 --- a/tests/js/fixtures/partial/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({"alltitles":{"sphinx_utils module":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"ar":0,"both":0,"built":0,"confirm":0,"document":0,"function":0,"html":0,"i":0,"includ":0,"input":0,"javascript":0,"match":0,"partial":0,"possibl":0,"project":0,"provid":0,"restructuredtext":0,"sampl":0,"search":0,"should":0,"term":0,"thi":0,"titl":0,"us":0,"when":0},"titles":["sphinx_utils module"],"titleterms":{"modul":0,"sphinx_util":0}}) \ No newline at end of file diff --git a/tests/js/fixtures/partial/searchindex.json b/tests/js/fixtures/partial/searchindex.json new file mode 100644 index 00000000000..a0a50be326e --- /dev/null +++ b/tests/js/fixtures/partial/searchindex.json @@ -0,0 +1 @@ +{"alltitles":{"sphinx_utils module":[[0,null]]},"docnames":["index"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst"],"indexentries":{},"objects":{},"objnames":{},"objtypes":{},"terms":{"ar":0,"both":0,"built":0,"confirm":0,"document":0,"function":0,"html":0,"i":0,"includ":0,"input":0,"javascript":0,"match":0,"partial":0,"possibl":0,"project":0,"provid":0,"restructuredtext":0,"sampl":0,"search":0,"should":0,"term":0,"thi":0,"titl":0,"us":0,"when":0},"titles":["sphinx_utils module"],"titleterms":{"modul":0,"sphinx_util":0}} \ No newline at end of file diff --git a/tests/js/fixtures/titles/searchindex.js b/tests/js/fixtures/titles/searchindex.js deleted file mode 100644 index 987be77992a..00000000000 --- a/tests/js/fixtures/titles/searchindex.js +++ /dev/null @@ -1 +0,0 @@ -Search.setIndex({"alltitles":{"Main Page":[[0,null]],"Relevance":[[0,"relevance"],[1,null]],"Result Scoring":[[0,"result-scoring"]]},"docnames":["index","relevance"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst","relevance.rst"],"indexentries":{"example (class in relevance)":[[0,"relevance.Example",false]],"module":[[0,"module-relevance",false]],"relevance":[[0,"index-1",false],[0,"module-relevance",false]],"relevance (relevance.example attribute)":[[0,"relevance.Example.relevance",false]],"scoring":[[0,"index-0",true]]},"objects":{"":[[0,0,0,"-","relevance"]],"relevance":[[0,1,1,"","Example"]],"relevance.Example":[[0,2,1,"","relevance"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:attribute"},"terms":{"":[0,1],"A":1,"By":0,"For":[0,1],"In":[0,1],"against":0,"align":0,"also":1,"an":0,"answer":0,"appear":1,"ar":1,"area":0,"ask":0,"assign":0,"attempt":0,"attribut":0,"both":0,"built":1,"can":[0,1],"class":0,"code":[0,1],"collect":0,"consid":1,"contain":0,"context":0,"corpu":1,"could":1,"demonstr":0,"describ":1,"detail":1,"determin":[0,1],"docstr":0,"document":[0,1],"domain":1,"dure":0,"engin":0,"evalu":0,"exampl":[0,1],"extract":0,"feedback":0,"find":0,"found":0,"from":0,"function":1,"ha":1,"handl":0,"happen":1,"head":0,"help":0,"highli":[0,1],"how":0,"i":[0,1],"improv":0,"inform":0,"intend":0,"issu":[0,1],"itself":1,"knowledg":0,"languag":1,"less":1,"like":[0,1],"mani":0,"match":0,"mention":1,"more":0,"name":[0,1],"numer":0,"object":0,"often":0,"one":[0,1],"onli":[0,1],"order":0,"other":0,"over":0,"page":1,"part":1,"particular":0,"present":0,"printf":1,"program":1,"project":0,"queri":[0,1],"question":0,"re":0,"rel":0,"research":0,"result":1,"retriev":0,"sai":0,"same":1,"search":[0,1],"seem":0,"softwar":1,"some":1,"sphinx":0,"straightforward":1,"subject":0,"subsect":0,"term":[0,1],"test":0,"text":0,"than":[0,1],"thei":0,"them":0,"thi":0,"time":0,"titl":0,"two":0,"typic":0,"us":0,"user":[0,1],"we":[0,1],"when":0,"whether":1,"which":0,"within":0,"word":0,"would":[0,1]},"titles":["Main Page","Relevance"],"titleterms":{"main":0,"page":0,"relev":[0,1],"result":0,"score":0}}) \ No newline at end of file diff --git a/tests/js/fixtures/titles/searchindex.json b/tests/js/fixtures/titles/searchindex.json new file mode 100644 index 00000000000..e94d9c6f83c --- /dev/null +++ b/tests/js/fixtures/titles/searchindex.json @@ -0,0 +1 @@ +{"alltitles":{"Main Page":[[0,null]],"Relevance":[[0,"relevance"],[1,null]],"Result Scoring":[[0,"result-scoring"]]},"docnames":["index","relevance"],"envversion":{"sphinx":64,"sphinx.domains.c":3,"sphinx.domains.changeset":1,"sphinx.domains.citation":1,"sphinx.domains.cpp":9,"sphinx.domains.index":1,"sphinx.domains.javascript":3,"sphinx.domains.math":2,"sphinx.domains.python":4,"sphinx.domains.rst":2,"sphinx.domains.std":2},"filenames":["index.rst","relevance.rst"],"indexentries":{"example (class in relevance)":[[0,"relevance.Example",false]],"module":[[0,"module-relevance",false]],"relevance":[[0,"index-1",false],[0,"module-relevance",false]],"relevance (relevance.example attribute)":[[0,"relevance.Example.relevance",false]],"scoring":[[0,"index-0",true]]},"objects":{"":[[0,0,0,"-","relevance"]],"relevance":[[0,1,1,"","Example"]],"relevance.Example":[[0,2,1,"","relevance"]]},"objnames":{"0":["py","module","Python module"],"1":["py","class","Python class"],"2":["py","attribute","Python attribute"]},"objtypes":{"0":"py:module","1":"py:class","2":"py:attribute"},"terms":{"":[0,1],"A":1,"By":0,"For":[0,1],"In":[0,1],"against":0,"align":0,"also":1,"an":0,"answer":0,"appear":1,"ar":1,"area":0,"ask":0,"assign":0,"attempt":0,"attribut":0,"both":0,"built":1,"can":[0,1],"class":0,"code":[0,1],"collect":0,"consid":1,"contain":0,"context":0,"corpu":1,"could":1,"demonstr":0,"describ":1,"detail":1,"determin":[0,1],"docstr":0,"document":[0,1],"domain":1,"dure":0,"engin":0,"evalu":0,"exampl":[0,1],"extract":0,"feedback":0,"find":0,"found":0,"from":0,"function":1,"ha":1,"handl":0,"happen":1,"head":0,"help":0,"highli":[0,1],"how":0,"i":[0,1],"improv":0,"inform":0,"intend":0,"issu":[0,1],"itself":1,"knowledg":0,"languag":1,"less":1,"like":[0,1],"mani":0,"match":0,"mention":1,"more":0,"name":[0,1],"numer":0,"object":0,"often":0,"one":[0,1],"onli":[0,1],"order":0,"other":0,"over":0,"page":1,"part":1,"particular":0,"present":0,"printf":1,"program":1,"project":0,"queri":[0,1],"question":0,"re":0,"rel":0,"research":0,"result":1,"retriev":0,"sai":0,"same":1,"search":[0,1],"seem":0,"softwar":1,"some":1,"sphinx":0,"straightforward":1,"subject":0,"subsect":0,"term":[0,1],"test":0,"text":0,"than":[0,1],"thei":0,"them":0,"thi":0,"time":0,"titl":0,"two":0,"typic":0,"us":0,"user":[0,1],"we":[0,1],"when":0,"whether":1,"which":0,"within":0,"word":0,"would":[0,1]},"titles":["Main Page","Relevance"],"titleterms":{"main":0,"page":0,"relev":[0,1],"result":0,"score":0}} \ No newline at end of file diff --git a/tests/js/jasmine-browser.mjs b/tests/js/jasmine-browser.mjs index b84217fd8c5..17b0c04ad34 100644 --- a/tests/js/jasmine-browser.mjs +++ b/tests/js/jasmine-browser.mjs @@ -4,7 +4,6 @@ export default { 'sphinx/themes/basic/static/doctools.js', 'sphinx/themes/basic/static/searchtools.js', 'sphinx/themes/basic/static/sphinx_highlight.js', - 'tests/js/fixtures/**/*.js', 'tests/js/documentation_options.js', 'tests/js/language_data.js', ], diff --git a/tests/js/roots/ecmascript/conf.py b/tests/js/roots/ecmascript/conf.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/js/roots/ecmascript/index.rst b/tests/js/roots/ecmascript/index.rst new file mode 100644 index 00000000000..aa0f23efb44 --- /dev/null +++ b/tests/js/roots/ecmascript/index.rst @@ -0,0 +1,6 @@ +ECMAScript +---------- + +This is a sample JavaScript (aka ``ECMAScript``) project used to generate a search engine index fixture. + +Use the `__proto__` property to access the `prototype `_ (if any) of an object instance. diff --git a/tests/js/searchtools.spec.js b/tests/js/searchtools.spec.js index cfe5fdcf7ed..1a3b73cbe8d 100644 --- a/tests/js/searchtools.spec.js +++ b/tests/js/searchtools.spec.js @@ -1,10 +1,10 @@ describe('Basic html theme search', function() { - function loadFixture(name) { + function loadIndex(name) { req = new XMLHttpRequest(); req.open("GET", `__src__/tests/js/fixtures/${name}`, false); req.send(null); - return req.responseText; + Search.setIndex(JSON.parse(req.responseText)); } function checkRanking(expectedRanking, results) { @@ -28,7 +28,7 @@ describe('Basic html theme search', function() { describe('terms search', function() { it('should find "C++" when in index', function() { - eval(loadFixture("cpp/searchindex.js")); + loadIndex("cpp/searchindex.json"); [_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('C++'); @@ -45,7 +45,7 @@ describe('Basic html theme search', function() { }); it('should be able to search for multiple terms', function() { - eval(loadFixture("multiterm/searchindex.js")); + loadIndex("multiterm/searchindex.json"); [_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('main page'); hits = [[ @@ -61,7 +61,7 @@ describe('Basic html theme search', function() { }); it('should partially-match "sphinx" when in title index', function() { - eval(loadFixture("partial/searchindex.js")); + loadIndex("partial/searchindex.json"); [_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('sphinx'); @@ -78,7 +78,7 @@ describe('Basic html theme search', function() { }); it('should partially-match within "possible" when in term index', function() { - eval(loadFixture("partial/searchindex.js")); + loadIndex("partial/searchindex.json"); [_searchQuery, searchterms, excluded, ..._remainingItems] = Search._parseQuery('ossibl'); terms = Search._index.terms; @@ -101,7 +101,7 @@ describe('Basic html theme search', function() { describe('aggregation of search results', function() { it('should combine document title and document term matches', function() { - eval(loadFixture("multiterm/searchindex.js")); + loadIndex("multiterm/searchindex.json"); searchParameters = Search._parseQuery('main page'); @@ -138,7 +138,7 @@ describe('Basic html theme search', function() { */ it('should score a code module match above a page-title match', function() { - eval(loadFixture("titles/searchindex.js")); + loadIndex("titles/searchindex.json"); expectedRanking = [ ['index', 'relevance', '#module-relevance'], /* py:module documentation */ @@ -152,7 +152,7 @@ describe('Basic html theme search', function() { }); it('should score a main-title match above an object member match', function() { - eval(loadFixture("titles/searchindex.js")); + loadIndex("titles/searchindex.json"); expectedRanking = [ ['relevance', 'Relevance', ''], /* main title */ @@ -166,7 +166,7 @@ describe('Basic html theme search', function() { }); it('should score a title match above a standard index entry match', function() { - eval(loadFixture("titles/searchindex.js")); + loadIndex("titles/searchindex.json"); expectedRanking = [ ['relevance', 'Relevance', ''], /* title */ @@ -180,7 +180,7 @@ describe('Basic html theme search', function() { }); it('should score a priority index entry match above a title match', function() { - eval(loadFixture("titles/searchindex.js")); + loadIndex("titles/searchindex.json"); expectedRanking = [ ['index', 'Main Page', '#index-0'], /* index entry */ @@ -194,7 +194,7 @@ describe('Basic html theme search', function() { }); it('should score a main-title match above a subheading-title match', function() { - eval(loadFixture("titles/searchindex.js")); + loadIndex("titles/searchindex.json"); expectedRanking = [ ['relevance', 'Relevance', ''], /* main title */ @@ -209,6 +209,38 @@ describe('Basic html theme search', function() { }); + describe('can handle edge-case search queries', function() { + + it('can search for the javascript prototype property', function() { + loadIndex("ecmascript/searchindex.json"); + + searchParameters = Search._parseQuery('__proto__'); + + hits = [ + [ + 'index', + 'ECMAScript', + '', + null, + 5, + 'index.rst', + 'text' + ] + ]; + expect(Search._performSearch(...searchParameters)).toEqual(hits); + }); + + it('does not find the javascript prototype property in unrelated documents', function() { + loadIndex("partial/searchindex.json"); + + searchParameters = Search._parseQuery('__proto__'); + + hits = []; + expect(Search._performSearch(...searchParameters)).toEqual(hits); + }); + + }); + }); describe("htmlToText", function() { diff --git a/tests/test_search.py b/tests/test_search.py index 600f66cb9f6..7b81c891c48 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -64,10 +64,7 @@ def get_objects(self) -> list[tuple[str, str, str, str, str, int]]: def load_searchindex(path: Path) -> Any: searchindex = path.read_text(encoding='utf8') - assert searchindex.startswith('Search.setIndex(') - assert searchindex.endswith(')') - - return json.loads(searchindex[16:-1]) + return json.loads(searchindex) def is_registered_term(index: Any, keyword: str) -> bool: @@ -90,7 +87,7 @@ def is_registered_term(index: Any, keyword: str) -> bool: @pytest.mark.sphinx('html', testroot='ext-viewcode') def test_objects_are_escaped(app): app.build(force_all=True) - index = load_searchindex(app.outdir / 'searchindex.js') + index = load_searchindex(app.outdir / 'searchindex.json') for item in index.get('objects').get(''): if item[-1] == 'n::Array<T, d>': # n::Array is escaped break @@ -101,7 +98,7 @@ def test_objects_are_escaped(app): @pytest.mark.sphinx('html', testroot='search') def test_meta_keys_are_handled_for_language_en(app): app.build(force_all=True) - searchindex = load_searchindex(app.outdir / 'searchindex.js') + searchindex = load_searchindex(app.outdir / 'searchindex.json') assert not is_registered_term(searchindex, 'thisnoteith') assert is_registered_term(searchindex, 'thisonetoo') assert is_registered_term(searchindex, 'findthiskei') @@ -119,7 +116,7 @@ def test_meta_keys_are_handled_for_language_en(app): ) def test_meta_keys_are_handled_for_language_de(app): app.build(force_all=True) - searchindex = load_searchindex(app.outdir / 'searchindex.js') + searchindex = load_searchindex(app.outdir / 'searchindex.json') assert not is_registered_term(searchindex, 'thisnoteith') assert is_registered_term(searchindex, 'thisonetoo') assert not is_registered_term(searchindex, 'findthiskei') @@ -132,14 +129,14 @@ def test_meta_keys_are_handled_for_language_de(app): @pytest.mark.sphinx('html', testroot='search') def test_stemmer_does_not_remove_short_words(app): app.build(force_all=True) - searchindex = (app.outdir / 'searchindex.js').read_text(encoding='utf8') + searchindex = (app.outdir / 'searchindex.json').read_text(encoding='utf8') assert 'bat' in searchindex @pytest.mark.sphinx('html', testroot='search') def test_stemmer(app): app.build(force_all=True) - searchindex = load_searchindex(app.outdir / 'searchindex.js') + searchindex = load_searchindex(app.outdir / 'searchindex.json') print(searchindex) assert is_registered_term(searchindex, 'findthisstemmedkei') assert is_registered_term(searchindex, 'intern') @@ -148,7 +145,7 @@ def test_stemmer(app): @pytest.mark.sphinx('html', testroot='search') def test_term_in_heading_and_section(app): app.build(force_all=True) - searchindex = (app.outdir / 'searchindex.js').read_text(encoding='utf8') + searchindex = (app.outdir / 'searchindex.json').read_text(encoding='utf8') # if search term is in the title of one doc and in the text of another # both documents should be a hit in the search index as a title, # respectively text hit @@ -159,7 +156,7 @@ def test_term_in_heading_and_section(app): @pytest.mark.sphinx('html', testroot='search') def test_term_in_raw_directive(app): app.build(force_all=True) - searchindex = load_searchindex(app.outdir / 'searchindex.js') + searchindex = load_searchindex(app.outdir / 'searchindex.json') assert not is_registered_term(searchindex, 'raw') assert is_registered_term(searchindex, 'rawword') assert not is_registered_term(searchindex, 'latex_keyword') @@ -380,7 +377,7 @@ def test_IndexBuilder_lookup(): ) def test_search_index_gen_zh(app): app.build(force_all=True) - index = load_searchindex(app.outdir / 'searchindex.js') + index = load_searchindex(app.outdir / 'searchindex.json') assert 'chinesetest ' not in index['terms'] assert 'chinesetest' in index['terms'] assert 'chinesetesttwo' in index['terms'] @@ -394,7 +391,7 @@ def test_search_index_gen_zh(app): ) def test_nosearch(app): app.build() - index = load_searchindex(app.outdir / 'searchindex.js') + index = load_searchindex(app.outdir / 'searchindex.json') assert index['docnames'] == ['index', 'nosearch', 'tocitem'] assert 'latex' not in index['terms'] assert 'bat' in index['terms'] @@ -411,14 +408,14 @@ def test_nosearch(app): ) def test_parallel(app): app.build() - index = load_searchindex(app.outdir / 'searchindex.js') + index = load_searchindex(app.outdir / 'searchindex.json') assert index['docnames'] == ['index', 'nosearch', 'tocitem'] @pytest.mark.sphinx('html', testroot='search') def test_search_index_is_deterministic(app): app.build(force_all=True) - index = load_searchindex(app.outdir / 'searchindex.js') + index = load_searchindex(app.outdir / 'searchindex.json') # Pretty print the index. Only shown by pytest on failure. print(f'searchindex.js contents:\n\n{json.dumps(index, indent=2)}') assert_is_sorted(index, '') @@ -470,9 +467,9 @@ def test_check_js_search_indexes(make_app, sphinx_test_tempdir, directory): ) app.build() - fresh_searchindex = app.outdir / 'searchindex.js' + fresh_searchindex = app.outdir / 'searchindex.json' existing_searchindex = ( - TESTS_ROOT / 'js' / 'fixtures' / directory.name / 'searchindex.js' + TESTS_ROOT / 'js' / 'fixtures' / directory.name / 'searchindex.json' ) msg = ( diff --git a/utils/generate_js_fixtures.py b/utils/generate_js_fixtures.py index ecf13a94741..df15c048ff6 100755 --- a/utils/generate_js_fixtures.py +++ b/utils/generate_js_fixtures.py @@ -27,8 +27,8 @@ def build(srcdir: Path) -> None: for directory in TEST_JS_ROOTS: - searchindex = directory / '_build' / 'searchindex.js' - destination = TEST_JS_FIXTURES / directory.name / 'searchindex.js' + searchindex = directory / '_build' / 'searchindex.json' + destination = TEST_JS_FIXTURES / directory.name / 'searchindex.json' print(f'Building {directory} ... ', end='') build(directory)