From 37e92ab564ad9bc9585e7a84d396ae97fc59aa32 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 3 Nov 2024 18:02:07 +0000 Subject: [PATCH 01/29] HTML search: sanitise `searchindex.js` contents * Remove the JavaScript `Object` prototype from most objects (with the exception of `Array` objects, where the prototype functions are useful). * Freeze the resulting search index entries (this does also prevent modifications to the previously mentioned `Array` objects). Note: some JavaScript built-in types such as `Set` and `Map` permit modifications to their contents even after they are frozen. However, they should not appear in the index contents since it is represented as JSON by the Python code in the `sphinx.search` module. --- sphinx/themes/basic/static/searchtools.js | 15 ++++++++---- tests/js/searchtools.spec.js | 29 +++++++++++++++++++++++ 2 files changed, 40 insertions(+), 4 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index aaf078d2b91..965f6df2101 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -219,7 +219,14 @@ const Search = { (document.body.appendChild(document.createElement("script")).src = url), setIndex: (index) => { - Search._index = index; + const sanitiseRecursive = function(obj) { + if (obj instanceof Object && !Array.isArray(obj)) { + Object.entries(obj).map(([k, v]) => obj[k] = sanitiseRecursive(v)); + return Object.freeze(Object.assign(Object.create(null), obj)); + } + return Object.freeze(obj); + } + Search._index = sanitiseRecursive(index); if (Search._queued_query !== null) { const query = Search._queued_query; Search._queued_query = null; @@ -474,7 +481,7 @@ const Search = { const descr = objName + _(", in ") + title; // add custom score for some objects according to scorer - if (Scorer.objPrio.hasOwnProperty(match[2])) + if (match[2] in Scorer.objPrio) score += Scorer.objPrio[match[2]]; else score += Scorer.objPrioDefault; @@ -520,13 +527,13 @@ const Search = { // add support for partial matches if (word.length > 2) { const escapedWord = _escapeRegExp(word); - if (!terms.hasOwnProperty(word)) { + if (!(word in terms)) { Object.keys(terms).forEach((term) => { if (term.match(escapedWord)) arr.push({ files: terms[term], score: Scorer.partialTerm }); }); } - if (!titleTerms.hasOwnProperty(word)) { + if (!(word in titleTerms)) { Object.keys(titleTerms).forEach((term) => { if (term.match(escapedWord)) arr.push({ files: titleTerms[term], score: Scorer.partialTitle }); diff --git a/tests/js/searchtools.spec.js b/tests/js/searchtools.spec.js index cfe5fdcf7ed..e234f1d6737 100644 --- a/tests/js/searchtools.spec.js +++ b/tests/js/searchtools.spec.js @@ -25,6 +25,35 @@ describe('Basic html theme search', function() { expect(nextExpected).toEqual(undefined); } + describe('index immutability', function() { + + it('should not be possible to modify index contents', function() { + eval(loadFixture("cpp/searchindex.js")); + + // record some initial state + const initialTitle = Search._index.titles[0]; + const initialTitlesProto = Search._index.titles.__proto__; + const initialTerms = Search._index.terms; + const initialDocNames = [...Search._index.docnames]; + + // attempt to mutate the index state + try { Search._index.docnames.pop(); } catch {}; + try { Search._index.docnames.push('extra'); } catch {}; + Search._index.titles[0] += 'modified'; + Search._index.titles.__proto__ = 'anotherProto'; + Search._index.terms = {'fake': [1, 2, 3]}; + Search._index.__proto__ = 'otherProto'; + + // ensure that none of the modifications were applied + expect(Search._index.__proto__).toBe(undefined); + expect(Search._index.terms).toEqual(initialTerms); + expect(Search._index.titles.__proto__).toEqual(initialTitlesProto); + expect(Search._index.titles[0]).toEqual(initialTitle); + expect(Search._index.docnames).toEqual(initialDocNames); + }); + + }); + describe('terms search', function() { it('should find "C++" when in index', function() { From 58ca7000dc2bbaeda556c74ebb8b5b74e6b62325 Mon Sep 17 00:00:00 2001 From: James Addison Date: Sun, 3 Nov 2024 19:23:14 +0000 Subject: [PATCH 02/29] Add CHANGES.rst entry [skip ci] --- CHANGES.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGES.rst b/CHANGES.rst index b47f417e9a1..2bd6aa3e471 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -15,6 +15,11 @@ Deprecated Features added -------------- +* #13098: HTML Search: the contents of the search index are sanitised + (reassigned to frozen null-prototype JavaScript objects), to reduce + the risk of unintended access or modification. + Patch by James Addison + Bugs fixed ---------- From 15545513fca15daae287489b160f9bc36162a3b5 Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 4 Nov 2024 21:37:14 +0000 Subject: [PATCH 03/29] Fixup: code-indentation correction --- sphinx/themes/basic/static/searchtools.js | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 965f6df2101..48d23ee1c6a 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -220,11 +220,11 @@ const Search = { setIndex: (index) => { const sanitiseRecursive = function(obj) { - if (obj instanceof Object && !Array.isArray(obj)) { - Object.entries(obj).map(([k, v]) => obj[k] = sanitiseRecursive(v)); - return Object.freeze(Object.assign(Object.create(null), obj)); - } - return Object.freeze(obj); + if (obj instanceof Object && !Array.isArray(obj)) { + Object.entries(obj).map(([k, v]) => obj[k] = sanitiseRecursive(v)); + return Object.freeze(Object.assign(Object.create(null), obj)); + } + return Object.freeze(obj); } Search._index = sanitiseRecursive(index); if (Search._queued_query !== null) { From 056c85bba248b6a700b376b1f91fa486832919ca Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 4 Nov 2024 21:47:30 +0000 Subject: [PATCH 04/29] Perf: refactor `sanitiseRecursive` As a side-effect, this introduces a limitation: the argument passed to the initial `sanitiseRecursive` call must be an `Object` itself. --- sphinx/themes/basic/static/searchtools.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 48d23ee1c6a..c496658c7e2 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -220,11 +220,12 @@ const Search = { setIndex: (index) => { const sanitiseRecursive = function(obj) { - if (obj instanceof Object && !Array.isArray(obj)) { - Object.entries(obj).map(([k, v]) => obj[k] = sanitiseRecursive(v)); - return Object.freeze(Object.assign(Object.create(null), obj)); + if (Array.isArray(obj)) return Object.freeze(obj); + const result = Object.create(null); + for (const [k, v] of Object.entries(obj)) { + result[k] = (v instanceof Object) ? sanitiseRecursive(v) : v; } - return Object.freeze(obj); + return Object.freeze(result); } Search._index = sanitiseRecursive(index); if (Search._queued_query !== null) { From 6a38587e73a9ce686e21008cdad228c10f34b33d Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 4 Nov 2024 21:57:50 +0000 Subject: [PATCH 05/29] Perf: additional `sanitiseRecursive` refactoring In particular, relocates the `Array.isArray` conditional clause to reduce the recursion depth by one level. --- sphinx/themes/basic/static/searchtools.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index c496658c7e2..47cdbe12cf0 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -220,10 +220,11 @@ const Search = { setIndex: (index) => { const sanitiseRecursive = function(obj) { - if (Array.isArray(obj)) return Object.freeze(obj); const result = Object.create(null); for (const [k, v] of Object.entries(obj)) { - result[k] = (v instanceof Object) ? sanitiseRecursive(v) : v; + if (!(v instanceof Object)) result[k] = v; + else if (Array.isArray(v)) result[k] = Object.freeze(v); + else result[k] = sanitiseRecursive(v); } return Object.freeze(result); } From 015ecb901be21379c659301d3576d7c95146dd6d Mon Sep 17 00:00:00 2001 From: James Addison Date: Mon, 4 Nov 2024 23:50:08 +0000 Subject: [PATCH 06/29] Prototype (unbenchmarked): remove nested-arrays from `searchindex.js` --- sphinx/search/__init__.py | 27 +++++++++++++++-------- sphinx/themes/basic/static/searchtools.js | 6 ++--- 2 files changed, 21 insertions(+), 12 deletions(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 3f19d3663a0..238d55462c7 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -9,6 +9,7 @@ import os import pickle import re +from collections import Counter from importlib import import_module from pathlib import Path from typing import IO, TYPE_CHECKING, Any @@ -358,10 +359,11 @@ def dump(self, stream: IO, format: Any) -> None: def get_objects( self, fn2index: dict[str, int] - ) -> dict[str, list[tuple[int, int, int, str, str]]]: - rv: dict[str, list[tuple[int, int, int, str, str]]] = {} + ) -> dict[str, dict[int, tuple[int, int, int, str, str]]]: + rv: dict[str, dict[int, tuple[int, int, int, str, str]]] = {} otypes = self._objtypes onames = self._objnames + prefix_count = Counter() for domain in self.env.domains.sorted(): sorted_objects = sorted(domain.get_objects()) for fullname, dispname, type, docname, anchor, prio in sorted_objects: @@ -372,7 +374,8 @@ def get_objects( fullname = html.escape(fullname) dispname = html.escape(dispname) prefix, _, name = dispname.rpartition('.') - plist = rv.setdefault(prefix, []) + index = prefix_count[prefix] + plist = rv.setdefault(prefix, {}) try: typeindex = otypes[domain.name, type] except KeyError: @@ -394,7 +397,8 @@ def get_objects( shortanchor = '-' else: shortanchor = anchor - plist.append((fn2index[docname], typeindex, prio, shortanchor, name)) + plist[index] = (fn2index[docname], typeindex, prio, shortanchor, name) + prefix_count[prefix] += 1 return rv def get_terms( @@ -429,19 +433,24 @@ def freeze(self) -> dict[str, Any]: objtypes = {v: k[0] + ':' + k[1] for (k, v) in self._objtypes.items()} objnames = self._objnames - alltitles: dict[str, list[tuple[int, str | None]]] = {} + alltitles_count = Counter() + alltitles: dict[str, dict[int, tuple[int, str | None]]] = {} for docname, titlelist in sorted(self._all_titles.items()): for title, titleid in titlelist: - alltitles.setdefault(title, []).append((fn2index[docname], titleid)) + count = alltitles_count[title] + alltitles.setdefault(title, {})[count] = (fn2index[docname], titleid) + alltitles_count[title] += 1 - index_entries: dict[str, list[tuple[int, str, bool]]] = {} + index_entry_count = Counter() + index_entries: dict[str, dict[int, tuple[int, str, bool]]] = {} for docname, entries in self._index_entries.items(): for entry, entry_id, main_entry in entries: - index_entries.setdefault(entry.lower(), []).append(( + count = index_entry_count[entry] + index_entries.setdefault(entry.lower(), {})[count] = ( fn2index[docname], entry_id, main_entry == 'main', - )) + ) return { 'docnames': docnames, diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 47cdbe12cf0..b769bf9014f 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -344,7 +344,7 @@ const Search = { const queryLower = query.toLowerCase().trim(); for (const [title, foundTitles] of Object.entries(allTitles)) { if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { - for (const [file, id] of foundTitles) { + for (const [file, id] of Object.values(foundTitles)) { const score = Math.round(Scorer.title * queryLower.length / title.length); const boost = titles[file] === title ? 1 : 0; // add a boost for document titles normalResults.push([ @@ -363,7 +363,7 @@ const Search = { // search for explicit entries in index directives for (const [entry, foundEntries] of Object.entries(indexEntries)) { if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { - for (const [file, id, isMain] of foundEntries) { + for (const [file, id, isMain] of Object.values(foundEntries)) { const score = Math.round(100 * queryLower.length / entry.length); const result = [ docNames[file], @@ -498,7 +498,7 @@ const Search = { ]); }; Object.keys(objects).forEach((prefix) => - objects[prefix].forEach((array) => + Object.values(objects[prefix]).forEach((array) => objectSearchCallback(prefix, array) ) ); From a3a1a310f028abca65966cd30bf91438be58aefd Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 00:09:53 +0000 Subject: [PATCH 07/29] Fixup: add missing counter-increment --- sphinx/search/__init__.py | 1 + 1 file changed, 1 insertion(+) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 238d55462c7..4215c4b46d2 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -451,6 +451,7 @@ def freeze(self) -> dict[str, Any]: entry_id, main_entry == 'main', ) + index_entry_count[entry] += 1 return { 'docnames': docnames, From db3aaf5aec61dad68b36e454ec39b551a0b844b2 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 00:10:05 +0000 Subject: [PATCH 08/29] Tests: regenerate HTML search test fixtures --- tests/js/fixtures/cpp/searchindex.js | 2 +- tests/js/fixtures/multiterm/searchindex.js | 2 +- tests/js/fixtures/partial/searchindex.js | 2 +- tests/js/fixtures/titles/searchindex.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js index e5837e65d56..235e099b0d3 100644 --- a/tests/js/fixtures/cpp/searchindex.js +++ b/tests/js/fixtures/cpp/searchindex.js @@ -1 +1 @@ -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 +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":[0,"_CPPv46Sphinx",false]}},"objects":{"":{"0":[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/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js index b3e2977792c..049c852bbd6 100644 --- a/tests/js/fixtures/multiterm/searchindex.js +++ b/tests/js/fixtures/multiterm/searchindex.js @@ -1 +1 @@ -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 +Search.setIndex({"alltitles":{"Main Page":{"0":[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 index ac024bf0c6e..358d70448c7 100644 --- a/tests/js/fixtures/partial/searchindex.js +++ b/tests/js/fixtures/partial/searchindex.js @@ -1 +1 @@ -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 +Search.setIndex({"alltitles":{"sphinx_utils module":{"0":[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 index 987be77992a..e88d002f2b6 100644 --- a/tests/js/fixtures/titles/searchindex.js +++ b/tests/js/fixtures/titles/searchindex.js @@ -1 +1 @@ -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 +Search.setIndex({"alltitles":{"Main Page":{"0":[0,null]},"Relevance":{"0":[0,"relevance"],"1":[1,null]},"Result Scoring":{"0":[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":[0,"relevance.Example",false]},"module":{"0":[0,"module-relevance",false]},"relevance":{"0":[0,"index-1",false],"1":[0,"module-relevance",false]},"relevance (relevance.example attribute)":{"0":[0,"relevance.Example.relevance",false]},"scoring":{"0":[0,"index-0",true]}},"objects":{"":{"0":[0,0,0,"-","relevance"]},"relevance":{"0":[0,1,1,"","Example"]},"relevance.Example":{"0":[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 From a72b1ff2055b2f75b3d891a3fb95efd526a3dc7b Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 00:19:51 +0000 Subject: [PATCH 09/29] Fixup: ignore entry-number keys when re-opening search index from file --- sphinx/search/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 4215c4b46d2..464cddc7718 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -335,7 +335,7 @@ def load(self, stream: IO, format: Any) -> None: for docname in self._titles: self._all_titles[docname] = [] for title, doc_tuples in frozen['alltitles'].items(): - for doc, titleid in doc_tuples: + for _, (doc, titleid) in doc_tuples.items(): self._all_titles[index2fn[doc]].append((title, titleid)) def load_terms(mapping: dict[str, Any]) -> dict[str, set[str]]: From 13581d19410d1a74e2593412683abcde2eebfa39 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 00:20:01 +0000 Subject: [PATCH 10/29] Tests: update expectations --- tests/test_search.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_search.py b/tests/test_search.py index 600f66cb9f6..90057026d4a 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -91,7 +91,7 @@ def is_registered_term(index: Any, keyword: str) -> bool: def test_objects_are_escaped(app): app.build(force_all=True) index = load_searchindex(app.outdir / 'searchindex.js') - for item in index.get('objects').get(''): + for _, item in index.get('objects').get('').items(): if item[-1] == 'n::Array<T, d>': # n::Array is escaped break else: @@ -236,10 +236,10 @@ def test_IndexBuilder(): 'envversion': '1.0', 'filenames': ['filename1_1', 'filename1_2', 'filename2_1', 'filename2_2'], 'objects': { - '': [ - (0, 0, 1, '#anchor', 'objdispname1'), - (2, 1, 1, '#anchor', 'objdispname1'), - ] + '': { + 0: (0, 0, 1, '#anchor', 'objdispname1'), + 1: (2, 1, 1, '#anchor', 'objdispname1'), + } }, 'objnames': { 0: ('dummy1', 'objtype1', 'objtype1'), @@ -260,13 +260,13 @@ def test_IndexBuilder(): 'section_titl': [0, 1, 2, 3], }, 'alltitles': { - 'another_title': [ - (0, 'another-title'), - (1, 'another-title'), - (2, 'another-title'), - (3, 'another-title'), - ], - 'section_title': [(0, None), (1, None), (2, None), (3, None)], + 'another_title': { + 0: (0, 'another-title'), + 1: (1, 'another-title'), + 2: (2, 'another-title'), + 3: (3, 'another-title'), + }, + 'section_title': {0: (0, None), 1: (1, None), 2: (2, None), 3: (3, None)}, }, 'indexentries': {}, } @@ -348,8 +348,8 @@ def test_IndexBuilder(): 'section_titl': [0, 1], }, 'alltitles': { - 'another_title': [(0, 'another-title'), (1, 'another-title')], - 'section_title': [(0, None), (1, None)], + 'another_title': {0: (0, 'another-title'), 1: (1, 'another-title')}, + 'section_title': {0: (0, None), 1: (1, None)}, }, 'indexentries': {}, } From 94857f74b7812b83e3d5b1c806d869765266a8de Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 00:30:00 +0000 Subject: [PATCH 11/29] Typing: add parameterized typehints to `Counter` variables --- sphinx/search/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 464cddc7718..fd257e8d32e 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -363,7 +363,7 @@ def get_objects( rv: dict[str, dict[int, tuple[int, int, int, str, str]]] = {} otypes = self._objtypes onames = self._objnames - prefix_count = Counter() + prefix_count: Counter[str] = Counter() for domain in self.env.domains.sorted(): sorted_objects = sorted(domain.get_objects()) for fullname, dispname, type, docname, anchor, prio in sorted_objects: @@ -433,7 +433,7 @@ def freeze(self) -> dict[str, Any]: objtypes = {v: k[0] + ':' + k[1] for (k, v) in self._objtypes.items()} objnames = self._objnames - alltitles_count = Counter() + alltitles_count: Counter[str] = Counter() alltitles: dict[str, dict[int, tuple[int, str | None]]] = {} for docname, titlelist in sorted(self._all_titles.items()): for title, titleid in titlelist: @@ -441,7 +441,7 @@ def freeze(self) -> dict[str, Any]: alltitles.setdefault(title, {})[count] = (fn2index[docname], titleid) alltitles_count[title] += 1 - index_entry_count = Counter() + index_entry_count: Counter[str] = Counter() index_entries: dict[str, dict[int, tuple[int, str, bool]]] = {} for docname, entries in self._index_entries.items(): for entry, entry_id, main_entry in entries: From ca7490d388302a2f7784a4a53fc2d8b3f1c50da4 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 00:31:03 +0000 Subject: [PATCH 12/29] Linting: resolve `ruff` PERF102 suggestions --- sphinx/search/__init__.py | 2 +- tests/test_search.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index fd257e8d32e..93d13572ff1 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -335,7 +335,7 @@ def load(self, stream: IO, format: Any) -> None: for docname in self._titles: self._all_titles[docname] = [] for title, doc_tuples in frozen['alltitles'].items(): - for _, (doc, titleid) in doc_tuples.items(): + for doc, titleid in doc_tuples.values(): self._all_titles[index2fn[doc]].append((title, titleid)) def load_terms(mapping: dict[str, Any]) -> dict[str, set[str]]: diff --git a/tests/test_search.py b/tests/test_search.py index 90057026d4a..10dac33da6e 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -91,7 +91,7 @@ def is_registered_term(index: Any, keyword: str) -> bool: def test_objects_are_escaped(app): app.build(force_all=True) index = load_searchindex(app.outdir / 'searchindex.js') - for _, item in index.get('objects').get('').items(): + for item in index.get('objects').get('').values(): if item[-1] == 'n::Array<T, d>': # n::Array is escaped break else: From 6df348fc31eb24f1cdf617018559d7faaee3bb98 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:35:56 +0000 Subject: [PATCH 13/29] Revert "Linting: resolve `ruff` PERF102 suggestions" This reverts commit ca7490d388302a2f7784a4a53fc2d8b3f1c50da4. --- sphinx/search/__init__.py | 2 +- tests/test_search.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 93d13572ff1..fd257e8d32e 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -335,7 +335,7 @@ def load(self, stream: IO, format: Any) -> None: for docname in self._titles: self._all_titles[docname] = [] for title, doc_tuples in frozen['alltitles'].items(): - for doc, titleid in doc_tuples.values(): + for _, (doc, titleid) in doc_tuples.items(): self._all_titles[index2fn[doc]].append((title, titleid)) def load_terms(mapping: dict[str, Any]) -> dict[str, set[str]]: diff --git a/tests/test_search.py b/tests/test_search.py index 10dac33da6e..90057026d4a 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -91,7 +91,7 @@ def is_registered_term(index: Any, keyword: str) -> bool: def test_objects_are_escaped(app): app.build(force_all=True) index = load_searchindex(app.outdir / 'searchindex.js') - for item in index.get('objects').get('').values(): + for _, item in index.get('objects').get('').items(): if item[-1] == 'n::Array<T, d>': # n::Array is escaped break else: From dab072f716b7311a5745ae3c15c1dec415df0f59 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:35:57 +0000 Subject: [PATCH 14/29] Revert "Typing: add parameterized typehints to `Counter` variables" This reverts commit 94857f74b7812b83e3d5b1c806d869765266a8de. --- sphinx/search/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index fd257e8d32e..464cddc7718 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -363,7 +363,7 @@ def get_objects( rv: dict[str, dict[int, tuple[int, int, int, str, str]]] = {} otypes = self._objtypes onames = self._objnames - prefix_count: Counter[str] = Counter() + prefix_count = Counter() for domain in self.env.domains.sorted(): sorted_objects = sorted(domain.get_objects()) for fullname, dispname, type, docname, anchor, prio in sorted_objects: @@ -433,7 +433,7 @@ def freeze(self) -> dict[str, Any]: objtypes = {v: k[0] + ':' + k[1] for (k, v) in self._objtypes.items()} objnames = self._objnames - alltitles_count: Counter[str] = Counter() + alltitles_count = Counter() alltitles: dict[str, dict[int, tuple[int, str | None]]] = {} for docname, titlelist in sorted(self._all_titles.items()): for title, titleid in titlelist: @@ -441,7 +441,7 @@ def freeze(self) -> dict[str, Any]: alltitles.setdefault(title, {})[count] = (fn2index[docname], titleid) alltitles_count[title] += 1 - index_entry_count: Counter[str] = Counter() + index_entry_count = Counter() index_entries: dict[str, dict[int, tuple[int, str, bool]]] = {} for docname, entries in self._index_entries.items(): for entry, entry_id, main_entry in entries: From dbafb5f7822baecc8242aaebf5a4d073c13a598d Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:35:58 +0000 Subject: [PATCH 15/29] Revert "Tests: update expectations" This reverts commit 13581d19410d1a74e2593412683abcde2eebfa39. --- tests/test_search.py | 28 ++++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/test_search.py b/tests/test_search.py index 90057026d4a..600f66cb9f6 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -91,7 +91,7 @@ def is_registered_term(index: Any, keyword: str) -> bool: def test_objects_are_escaped(app): app.build(force_all=True) index = load_searchindex(app.outdir / 'searchindex.js') - for _, item in index.get('objects').get('').items(): + for item in index.get('objects').get(''): if item[-1] == 'n::Array<T, d>': # n::Array is escaped break else: @@ -236,10 +236,10 @@ def test_IndexBuilder(): 'envversion': '1.0', 'filenames': ['filename1_1', 'filename1_2', 'filename2_1', 'filename2_2'], 'objects': { - '': { - 0: (0, 0, 1, '#anchor', 'objdispname1'), - 1: (2, 1, 1, '#anchor', 'objdispname1'), - } + '': [ + (0, 0, 1, '#anchor', 'objdispname1'), + (2, 1, 1, '#anchor', 'objdispname1'), + ] }, 'objnames': { 0: ('dummy1', 'objtype1', 'objtype1'), @@ -260,13 +260,13 @@ def test_IndexBuilder(): 'section_titl': [0, 1, 2, 3], }, 'alltitles': { - 'another_title': { - 0: (0, 'another-title'), - 1: (1, 'another-title'), - 2: (2, 'another-title'), - 3: (3, 'another-title'), - }, - 'section_title': {0: (0, None), 1: (1, None), 2: (2, None), 3: (3, None)}, + 'another_title': [ + (0, 'another-title'), + (1, 'another-title'), + (2, 'another-title'), + (3, 'another-title'), + ], + 'section_title': [(0, None), (1, None), (2, None), (3, None)], }, 'indexentries': {}, } @@ -348,8 +348,8 @@ def test_IndexBuilder(): 'section_titl': [0, 1], }, 'alltitles': { - 'another_title': {0: (0, 'another-title'), 1: (1, 'another-title')}, - 'section_title': {0: (0, None), 1: (1, None)}, + 'another_title': [(0, 'another-title'), (1, 'another-title')], + 'section_title': [(0, None), (1, None)], }, 'indexentries': {}, } From 5ab5f1d3783e161647e080d7491fb174e2d013af Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:35:58 +0000 Subject: [PATCH 16/29] Revert "Fixup: ignore entry-number keys when re-opening search index from file" This reverts commit a72b1ff2055b2f75b3d891a3fb95efd526a3dc7b. --- sphinx/search/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 464cddc7718..4215c4b46d2 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -335,7 +335,7 @@ def load(self, stream: IO, format: Any) -> None: for docname in self._titles: self._all_titles[docname] = [] for title, doc_tuples in frozen['alltitles'].items(): - for _, (doc, titleid) in doc_tuples.items(): + for doc, titleid in doc_tuples: self._all_titles[index2fn[doc]].append((title, titleid)) def load_terms(mapping: dict[str, Any]) -> dict[str, set[str]]: From 7a756472282505a019b062c06c68eab81356f27a Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:35:59 +0000 Subject: [PATCH 17/29] Revert "Tests: regenerate HTML search test fixtures" This reverts commit db3aaf5aec61dad68b36e454ec39b551a0b844b2. --- tests/js/fixtures/cpp/searchindex.js | 2 +- tests/js/fixtures/multiterm/searchindex.js | 2 +- tests/js/fixtures/partial/searchindex.js | 2 +- tests/js/fixtures/titles/searchindex.js | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/js/fixtures/cpp/searchindex.js b/tests/js/fixtures/cpp/searchindex.js index 235e099b0d3..e5837e65d56 100644 --- a/tests/js/fixtures/cpp/searchindex.js +++ b/tests/js/fixtures/cpp/searchindex.js @@ -1 +1 @@ -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":[0,"_CPPv46Sphinx",false]}},"objects":{"":{"0":[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 +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/multiterm/searchindex.js b/tests/js/fixtures/multiterm/searchindex.js index 049c852bbd6..b3e2977792c 100644 --- a/tests/js/fixtures/multiterm/searchindex.js +++ b/tests/js/fixtures/multiterm/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"Main Page":{"0":[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 +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/partial/searchindex.js b/tests/js/fixtures/partial/searchindex.js index 358d70448c7..ac024bf0c6e 100644 --- a/tests/js/fixtures/partial/searchindex.js +++ b/tests/js/fixtures/partial/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"sphinx_utils module":{"0":[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 +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/titles/searchindex.js b/tests/js/fixtures/titles/searchindex.js index e88d002f2b6..987be77992a 100644 --- a/tests/js/fixtures/titles/searchindex.js +++ b/tests/js/fixtures/titles/searchindex.js @@ -1 +1 @@ -Search.setIndex({"alltitles":{"Main Page":{"0":[0,null]},"Relevance":{"0":[0,"relevance"],"1":[1,null]},"Result Scoring":{"0":[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":[0,"relevance.Example",false]},"module":{"0":[0,"module-relevance",false]},"relevance":{"0":[0,"index-1",false],"1":[0,"module-relevance",false]},"relevance (relevance.example attribute)":{"0":[0,"relevance.Example.relevance",false]},"scoring":{"0":[0,"index-0",true]}},"objects":{"":{"0":[0,0,0,"-","relevance"]},"relevance":{"0":[0,1,1,"","Example"]},"relevance.Example":{"0":[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 +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 From 8c2f7e290f58700c73d9c2e8c0c29934a7fa6e61 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:36:00 +0000 Subject: [PATCH 18/29] Revert "Fixup: add missing counter-increment" This reverts commit a3a1a310f028abca65966cd30bf91438be58aefd. --- sphinx/search/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 4215c4b46d2..238d55462c7 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -451,7 +451,6 @@ def freeze(self) -> dict[str, Any]: entry_id, main_entry == 'main', ) - index_entry_count[entry] += 1 return { 'docnames': docnames, From 26ec58911843621d72a60ec2988768ee37030650 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:36:01 +0000 Subject: [PATCH 19/29] Revert "Prototype (unbenchmarked): remove nested-arrays from `searchindex.js`" This reverts commit 015ecb901be21379c659301d3576d7c95146dd6d. --- sphinx/search/__init__.py | 27 ++++++++--------------- sphinx/themes/basic/static/searchtools.js | 6 ++--- 2 files changed, 12 insertions(+), 21 deletions(-) diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index 238d55462c7..3f19d3663a0 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -9,7 +9,6 @@ import os import pickle import re -from collections import Counter from importlib import import_module from pathlib import Path from typing import IO, TYPE_CHECKING, Any @@ -359,11 +358,10 @@ def dump(self, stream: IO, format: Any) -> None: def get_objects( self, fn2index: dict[str, int] - ) -> dict[str, dict[int, tuple[int, int, int, str, str]]]: - rv: dict[str, dict[int, tuple[int, int, int, str, str]]] = {} + ) -> dict[str, list[tuple[int, int, int, str, str]]]: + rv: dict[str, list[tuple[int, int, int, str, str]]] = {} otypes = self._objtypes onames = self._objnames - prefix_count = Counter() for domain in self.env.domains.sorted(): sorted_objects = sorted(domain.get_objects()) for fullname, dispname, type, docname, anchor, prio in sorted_objects: @@ -374,8 +372,7 @@ def get_objects( fullname = html.escape(fullname) dispname = html.escape(dispname) prefix, _, name = dispname.rpartition('.') - index = prefix_count[prefix] - plist = rv.setdefault(prefix, {}) + plist = rv.setdefault(prefix, []) try: typeindex = otypes[domain.name, type] except KeyError: @@ -397,8 +394,7 @@ def get_objects( shortanchor = '-' else: shortanchor = anchor - plist[index] = (fn2index[docname], typeindex, prio, shortanchor, name) - prefix_count[prefix] += 1 + plist.append((fn2index[docname], typeindex, prio, shortanchor, name)) return rv def get_terms( @@ -433,24 +429,19 @@ def freeze(self) -> dict[str, Any]: objtypes = {v: k[0] + ':' + k[1] for (k, v) in self._objtypes.items()} objnames = self._objnames - alltitles_count = Counter() - alltitles: dict[str, dict[int, tuple[int, str | None]]] = {} + alltitles: dict[str, list[tuple[int, str | None]]] = {} for docname, titlelist in sorted(self._all_titles.items()): for title, titleid in titlelist: - count = alltitles_count[title] - alltitles.setdefault(title, {})[count] = (fn2index[docname], titleid) - alltitles_count[title] += 1 + alltitles.setdefault(title, []).append((fn2index[docname], titleid)) - index_entry_count = Counter() - index_entries: dict[str, dict[int, tuple[int, str, bool]]] = {} + index_entries: dict[str, list[tuple[int, str, bool]]] = {} for docname, entries in self._index_entries.items(): for entry, entry_id, main_entry in entries: - count = index_entry_count[entry] - index_entries.setdefault(entry.lower(), {})[count] = ( + index_entries.setdefault(entry.lower(), []).append(( fn2index[docname], entry_id, main_entry == 'main', - ) + )) return { 'docnames': docnames, diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index b769bf9014f..47cdbe12cf0 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -344,7 +344,7 @@ const Search = { const queryLower = query.toLowerCase().trim(); for (const [title, foundTitles] of Object.entries(allTitles)) { if (title.toLowerCase().trim().includes(queryLower) && (queryLower.length >= title.length/2)) { - for (const [file, id] of Object.values(foundTitles)) { + for (const [file, id] of foundTitles) { const score = Math.round(Scorer.title * queryLower.length / title.length); const boost = titles[file] === title ? 1 : 0; // add a boost for document titles normalResults.push([ @@ -363,7 +363,7 @@ const Search = { // search for explicit entries in index directives for (const [entry, foundEntries] of Object.entries(indexEntries)) { if (entry.includes(queryLower) && (queryLower.length >= entry.length/2)) { - for (const [file, id, isMain] of Object.values(foundEntries)) { + for (const [file, id, isMain] of foundEntries) { const score = Math.round(100 * queryLower.length / entry.length); const result = [ docNames[file], @@ -498,7 +498,7 @@ const Search = { ]); }; Object.keys(objects).forEach((prefix) => - Object.values(objects[prefix]).forEach((array) => + objects[prefix].forEach((array) => objectSearchCallback(prefix, array) ) ); From c0af10289858ad10d2ec5d803268ecdfccf76874 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:44:59 +0000 Subject: [PATCH 20/29] Tests: add test coverage to demonstrate mutating a nested array element --- tests/js/searchtools.spec.js | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/js/searchtools.spec.js b/tests/js/searchtools.spec.js index e234f1d6737..13da533f6c0 100644 --- a/tests/js/searchtools.spec.js +++ b/tests/js/searchtools.spec.js @@ -35,8 +35,11 @@ describe('Basic html theme search', function() { const initialTitlesProto = Search._index.titles.__proto__; const initialTerms = Search._index.terms; const initialDocNames = [...Search._index.docnames]; + const initialObject = [...Search._index.objects[''][0]]; // attempt to mutate the index state + try { Search._index.objects[''][0].pop(); } catch {}; + try { Search._index.objects[''][0].push('extra'); } catch {}; try { Search._index.docnames.pop(); } catch {}; try { Search._index.docnames.push('extra'); } catch {}; Search._index.titles[0] += 'modified'; @@ -50,6 +53,7 @@ describe('Basic html theme search', function() { expect(Search._index.titles.__proto__).toEqual(initialTitlesProto); expect(Search._index.titles[0]).toEqual(initialTitle); expect(Search._index.docnames).toEqual(initialDocNames); + expect(Search._index.objects[''][0]).toEqual(initialObject); }); }); From 0248ccc86f71e69c7b8ad6f69ee6ef0e8518765f Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 11:50:14 +0000 Subject: [PATCH 21/29] sanitiseRecursive: implement recursive array sanitisation --- sphinx/themes/basic/static/searchtools.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 47cdbe12cf0..7081dbcdfb8 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -220,11 +220,11 @@ const Search = { setIndex: (index) => { const sanitiseRecursive = function(obj) { + if (!(obj instanceof Object)) return obj; + if (Array.isArray(obj)) return Object.freeze(obj.map(v => sanitiseRecursive(v))); const result = Object.create(null); for (const [k, v] of Object.entries(obj)) { - if (!(v instanceof Object)) result[k] = v; - else if (Array.isArray(v)) result[k] = Object.freeze(v); - else result[k] = sanitiseRecursive(v); + result[k] = sanitiseRecursive(v); } return Object.freeze(result); } From 6542c8bc41732175805bfa0b4f359e92c2aebbae Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 12:44:22 +0000 Subject: [PATCH 22/29] Refactor: extract common `evaluate` function path --- sphinx/themes/basic/static/searchtools.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 7081dbcdfb8..80c21c8b1aa 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -220,11 +220,11 @@ const Search = { setIndex: (index) => { const sanitiseRecursive = function(obj) { - if (!(obj instanceof Object)) return obj; - if (Array.isArray(obj)) return Object.freeze(obj.map(v => sanitiseRecursive(v))); + const evaluate = x => (x instanceof Object) ? sanitiseRecursive(x) : x; + if (Array.isArray(obj)) return Object.freeze(obj.map(evaluate)); const result = Object.create(null); for (const [k, v] of Object.entries(obj)) { - result[k] = sanitiseRecursive(v); + result[k] = evaluate(v); } return Object.freeze(result); } From 2ada40e8910242462a66833c40f3c23010e2ff44 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 12:47:10 +0000 Subject: [PATCH 23/29] Refactor: avoid possibiliy of repeat `evaluate` construction --- sphinx/themes/basic/static/searchtools.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 80c21c8b1aa..3c839ee7b04 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -219,8 +219,8 @@ const Search = { (document.body.appendChild(document.createElement("script")).src = url), setIndex: (index) => { + const evaluate = x => (x instanceof Object) ? sanitiseRecursive(x) : x; const sanitiseRecursive = function(obj) { - const evaluate = x => (x instanceof Object) ? sanitiseRecursive(x) : x; if (Array.isArray(obj)) return Object.freeze(obj.map(evaluate)); const result = Object.create(null); for (const [k, v] of Object.entries(obj)) { From e6ecded2a6011e6a0d96f72a9a7d9c3586fa630d Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 13:05:25 +0000 Subject: [PATCH 24/29] Refactor: `sanitiseRecursive` brevity/performance --- sphinx/themes/basic/static/searchtools.js | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 3c839ee7b04..7d2cb8fc33e 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -221,12 +221,9 @@ const Search = { setIndex: (index) => { const evaluate = x => (x instanceof Object) ? sanitiseRecursive(x) : x; const sanitiseRecursive = function(obj) { - if (Array.isArray(obj)) return Object.freeze(obj.map(evaluate)); - const result = Object.create(null); - for (const [k, v] of Object.entries(obj)) { - result[k] = evaluate(v); - } - return Object.freeze(result); + Object.values(obj).map(evaluate); + if (!Array.isArray(obj)) Object.setPrototypeOf(obj, null); + return Object.freeze(obj); } Search._index = sanitiseRecursive(index); if (Search._queued_query !== null) { From 6b4b67509937af82911c041255b60e0eab23ad26 Mon Sep 17 00:00:00 2001 From: James Addison Date: Tue, 5 Nov 2024 14:04:52 +0000 Subject: [PATCH 25/29] Refactor: translate from `sanitiseRecursive` to an iterative approach --- sphinx/themes/basic/static/searchtools.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 7d2cb8fc33e..a3aed1c4d3e 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -219,13 +219,15 @@ const Search = { (document.body.appendChild(document.createElement("script")).src = url), setIndex: (index) => { - const evaluate = x => (x instanceof Object) ? sanitiseRecursive(x) : x; - const sanitiseRecursive = function(obj) { - Object.values(obj).map(evaluate); - if (!Array.isArray(obj)) Object.setPrototypeOf(obj, null); - return Object.freeze(obj); + const stack = [index]; + while (stack.length) { + const value = stack.pop(); + if (!(value instanceof Object)) continue; + stack.push(...Object.values(value)); + if (!Array.isArray(value)) Object.setPrototypeOf(value, null); + Object.freeze(value); } - Search._index = sanitiseRecursive(index); + Search._index = index; if (Search._queued_query !== null) { const query = Search._queued_query; Search._queued_query = null; From ab841c669c155dae517477e4618cf619082e55de Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 6 Nov 2024 11:27:13 +0000 Subject: [PATCH 26/29] Performance: avoid `Object.values` call for `Array` instances --- sphinx/themes/basic/static/searchtools.js | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index a3aed1c4d3e..183ce670b11 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -223,8 +223,12 @@ const Search = { while (stack.length) { const value = stack.pop(); if (!(value instanceof Object)) continue; - stack.push(...Object.values(value)); - if (!Array.isArray(value)) Object.setPrototypeOf(value, null); + if (Array.isArray(value)) { + stack.push(...value); + } else { + stack.push(...Object.values(value)); + Object.setPrototypeOf(value, null); + } Object.freeze(value); } Search._index = index; From 0e61a3c50653f23335c3c2aba43ce65ed72f0d56 Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 6 Nov 2024 11:37:17 +0000 Subject: [PATCH 27/29] Performance: omit non-`Object` items from the stack --- sphinx/themes/basic/static/searchtools.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index 183ce670b11..cd8bf460190 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -220,13 +220,13 @@ const Search = { setIndex: (index) => { const stack = [index]; + const isObject = x => x instanceof Object; while (stack.length) { const value = stack.pop(); - if (!(value instanceof Object)) continue; if (Array.isArray(value)) { - stack.push(...value); + stack.push(...value.filter(isObject)); } else { - stack.push(...Object.values(value)); + stack.push(...Object.values(value).filter(isObject)); Object.setPrototypeOf(value, null); } Object.freeze(value); From b2de18ad425da3deeb597b2eb98b6209ca712cdd Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 6 Nov 2024 11:39:41 +0000 Subject: [PATCH 28/29] Refactor: rectify variable name Relates-to commit 0e61a3c50653f23335c3c2aba43ce65ed72f0d56. [skip ci] --- sphinx/themes/basic/static/searchtools.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index cd8bf460190..de02bd4859e 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -222,14 +222,14 @@ const Search = { const stack = [index]; const isObject = x => x instanceof Object; while (stack.length) { - const value = stack.pop(); - if (Array.isArray(value)) { - stack.push(...value.filter(isObject)); + const obj = stack.pop(); + if (Array.isArray(obj)) { + stack.push(...obj.filter(isObject)); } else { - stack.push(...Object.values(value).filter(isObject)); - Object.setPrototypeOf(value, null); + stack.push(...Object.values(obj).filter(isObject)); + Object.setPrototypeOf(obj, null); } - Object.freeze(value); + Object.freeze(obj); } Search._index = index; if (Search._queued_query !== null) { From 6aa4f7f24e3c45e238621fbb8fed3bd986be2940 Mon Sep 17 00:00:00 2001 From: James Addison Date: Wed, 6 Nov 2024 12:13:12 +0000 Subject: [PATCH 29/29] Refactor / performance: consolidate non-emptiness check with stack pop operation --- sphinx/themes/basic/static/searchtools.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index de02bd4859e..b2f75e6ab8b 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -221,8 +221,8 @@ const Search = { setIndex: (index) => { const stack = [index]; const isObject = x => x instanceof Object; - while (stack.length) { - const obj = stack.pop(); + let obj; + while (obj = stack.pop()) { if (Array.isArray(obj)) { stack.push(...obj.filter(isObject)); } else {