diff --git a/sphinx/search/__init__.py b/sphinx/search/__init__.py index e4fdeec9102..95a0e922d53 100644 --- a/sphinx/search/__init__.py +++ b/sphinx/search/__init__.py @@ -183,6 +183,7 @@ class WordCollector(nodes.NodeVisitor): def __init__(self, document: nodes.document, lang: SearchLanguage) -> None: super().__init__(document) self.found_words: List[str] = [] + self.found_titles: List[str] = [] self.found_title_words: List[str] = [] self.lang = lang @@ -213,7 +214,9 @@ def dispatch_visit(self, node: Node) -> None: elif isinstance(node, nodes.Text): self.found_words.extend(self.lang.split(node.astext())) elif isinstance(node, nodes.title): - self.found_title_words.extend(self.lang.split(node.astext())) + title = node.astext() + self.found_titles.append(title) + self.found_title_words.extend(self.lang.split(title)) elif isinstance(node, Element) and self.is_meta_keywords(node): keywords = node['content'] keywords = [keyword.strip() for keyword in keywords.split(',')] @@ -237,6 +240,7 @@ def __init__(self, env: BuildEnvironment, lang: str, options: Dict, scoring: str self._mapping: Dict[str, Set[str]] = {} # stemmed word -> set(docname) # stemmed words in titles -> set(docname) self._title_mapping: Dict[str, Set[str]] = {} + self._all_titles: Dict[str, List[str]] = {} # docname -> all titles self._stem_cache: Dict[str, str] = {} # word -> stemmed word self._objtypes: Dict[Tuple[str, str], int] = {} # objtype -> index # objtype index -> (domain, type, objname (localized)) @@ -281,6 +285,11 @@ def load(self, stream: IO, format: Any) -> None: index2fn = frozen['docnames'] self._filenames = dict(zip(index2fn, frozen['filenames'])) self._titles = dict(zip(index2fn, frozen['titles'])) + self._all_titles = {} + + for title, docs in frozen['alltitles'].items(): + for doc in docs: + self._all_titles.setdefault(index2fn[doc], []).append(title) def load_terms(mapping: Dict[str, Any]) -> Dict[str, Set[str]]: rv = {} @@ -364,9 +373,16 @@ def freeze(self) -> Dict[str, Any]: objects = self.get_objects(fn2index) # populates _objtypes objtypes = {v: k[0] + ':' + k[1] for (k, v) in self._objtypes.items()} objnames = self._objnames + + alltitles: Dict[str, List[int]] = {} + for docname, titlelist in self._all_titles.items(): + for title in titlelist: + alltitles.setdefault(title.lower(), []).append(fn2index[docname]) + return dict(docnames=docnames, filenames=filenames, titles=titles, terms=terms, objects=objects, objtypes=objtypes, objnames=objnames, - titleterms=title_terms, envversion=self.env.version) + titleterms=title_terms, envversion=self.env.version, + alltitles=alltitles) def label(self) -> str: return "%s (code: %s)" % (self.lang.language_name, self.lang.lang) @@ -374,13 +390,16 @@ def label(self) -> str: def prune(self, docnames: Iterable[str]) -> None: """Remove data for all docnames not in the list.""" new_titles = {} + new_alltitles = {} new_filenames = {} for docname in docnames: if docname in self._titles: new_titles[docname] = self._titles[docname] + new_alltitles[docname] = self._all_titles[docname] new_filenames[docname] = self._filenames[docname] self._titles = new_titles self._filenames = new_filenames + self._all_titles = new_alltitles for wordnames in self._mapping.values(): wordnames.intersection_update(docnames) for wordnames in self._title_mapping.values(): @@ -403,6 +422,8 @@ def stem(word: str) -> str: return self._stem_cache[word] _filter = self.lang.word_filter + self._all_titles[docname] = visitor.found_titles + for word in visitor.found_title_words: stemmed_word = stem(word) if _filter(stemmed_word): diff --git a/sphinx/themes/basic/static/searchtools.js b/sphinx/themes/basic/static/searchtools.js index f2fb7d5cf7e..23d96de5ede 100644 --- a/sphinx/themes/basic/static/searchtools.js +++ b/sphinx/themes/basic/static/searchtools.js @@ -237,6 +237,11 @@ const Search = { * execute search (requires search index to be loaded) */ query: (query) => { + const docNames = Search._index.docnames; + const filenames = Search._index.filenames; + const titles = Search._index.titles; + const allTitles = Search._index.alltitles; + // stem the search terms and add them to the correct list const stemmer = new Stemmer(); const searchTerms = new Set(); @@ -272,6 +277,19 @@ const Search = { let results = []; _removeChildren(document.getElementById("search-progress")); + const queryLower = query.toLowerCase(); + if (allTitles[queryLower]) + allTitles[queryLower].forEach((titlematch) => { + results.push([ + docNames[titlematch], + titles[titlematch], + "", + null, + 1000, + filenames[titlematch], + ]); + }) + // lookup as object objectTerms.forEach((term) => results.push(...Search.performObjectSearch(term, objectTerms)) diff --git a/tests/test_search.py b/tests/test_search.py index 0330bfbae16..df8b78b0bff 100644 --- a/tests/test_search.py +++ b/tests/test_search.py @@ -177,7 +177,8 @@ def test_IndexBuilder(): 'non': [0, 1, 2, 3], 'test': [0, 1, 2, 3]}, 'titles': ('title1_1', 'title1_2', 'title2_1', 'title2_2'), - 'titleterms': {'section_titl': [0, 1, 2, 3]} + 'titleterms': {'section_titl': [0, 1, 2, 3]}, + 'alltitles': {'section_title': [0, 1, 2, 3]} } assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1} assert index._objnames == {0: ('dummy1', 'objtype1', 'objtype1'), @@ -234,7 +235,8 @@ def test_IndexBuilder(): 'non': [0, 1], 'test': [0, 1]}, 'titles': ('title1_2', 'title2_2'), - 'titleterms': {'section_titl': [0, 1]} + 'titleterms': {'section_titl': [0, 1]}, + 'alltitles': {'section_title': [0, 1]} } assert index._objtypes == {('dummy1', 'objtype1'): 0, ('dummy2', 'objtype1'): 1} assert index._objnames == {0: ('dummy1', 'objtype1', 'objtype1'),