|
20 | 20 | # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
21 | 21 | # SOFTWARE.
|
22 | 22 |
|
23 |
| -"""Module to fuse search.""" |
| 23 | +"""Module for generating search indices.""" |
24 | 24 |
|
25 | 25 | import json
|
26 | 26 | import re
|
|
29 | 29 |
|
30 | 30 |
|
31 | 31 | class SearchIndex:
|
32 |
| - """Class to get search index.""" |
| 32 | + """Generate a search index for a Sphinx document.""" |
33 | 33 |
|
34 | 34 | def __init__(self, doc_name, app):
|
35 |
| - """Initialize the class. |
| 35 | + """ |
| 36 | + Initialize the search index object. |
36 | 37 |
|
37 | 38 | Parameters
|
38 | 39 | ----------
|
39 | 40 | doc_name : str
|
40 |
| - Document name. |
| 41 | + Name of the document. |
41 | 42 | app : Sphinx
|
42 |
| - Sphinx application. |
43 |
| - version : str |
44 |
| - Version of the document for prefixing the path. |
| 43 | + Sphinx application instance. |
45 | 44 | """
|
46 |
| - self._doc_name = doc_name |
47 |
| - self.doc_path = f"{self._doc_name}.html" |
48 |
| - self.doc_title = app.env.titles[self._doc_name].astext() |
49 |
| - self._doc_tree = app.env.get_doctree(self._doc_name) |
| 45 | + self.doc_name = doc_name |
| 46 | + self.doc_path = f"{self.doc_name}.html" |
| 47 | + self.env = app.env |
| 48 | + self.doc_title = self.env.titles[self.doc_name].astext() |
| 49 | + self.doc_tree = self.env.get_doctree(self.doc_name) |
50 | 50 | self.sections = []
|
51 | 51 |
|
52 |
| - def iterate_through_docs(self): |
53 |
| - """Iterate through the document.""" |
54 |
| - for node in self._doc_tree.traverse(nodes.section): |
55 |
| - self.section_title = node[0].astext() |
56 |
| - self.section_text = "\n".join( |
| 52 | + def build_sections(self): |
| 53 | + """Build sections from the document tree.""" |
| 54 | + for node in self.doc_tree.traverse(nodes.section): |
| 55 | + section_title = node[0].astext() |
| 56 | + section_text = "\n".join( |
57 | 57 | n.astext()
|
58 | 58 | for node_type in [nodes.paragraph, nodes.literal_block]
|
59 | 59 | for n in node.traverse(node_type)
|
60 | 60 | )
|
61 |
| - self.section_anchor_id = _title_to_anchor(self.section_title) |
| 61 | + section_anchor_id = _title_to_anchor(section_title) |
62 | 62 | self.sections.append(
|
63 | 63 | {
|
64 |
| - "section_title": self.section_title, |
65 |
| - "section_text": self.section_text, |
66 |
| - "section_anchor_id": self.section_anchor_id, |
| 64 | + "title": section_title, |
| 65 | + "text": section_text, |
| 66 | + "anchor_id": section_anchor_id, |
67 | 67 | }
|
68 | 68 | )
|
69 | 69 |
|
| 70 | + def generate_breadcrumbs(self, section_title: str) -> str: |
| 71 | + """ |
| 72 | + Generate title breadcrumbs from the document structure. |
| 73 | +
|
| 74 | + Parameters |
| 75 | + ---------- |
| 76 | + section_title : str |
| 77 | + The section title to generate breadcrumbs for. |
| 78 | +
|
| 79 | + Returns |
| 80 | + ------- |
| 81 | + str |
| 82 | + Breadcrumb string. |
| 83 | +
|
| 84 | + Notes |
| 85 | + ----- |
| 86 | + The last part of `doc_name` is ignored because it represents the file name, |
| 87 | + which is not needed for the breadcrumb trail. The breadcrumb trail is generated |
| 88 | + by iterating over the parts of `doc_name` and fetching the title from the environment. |
| 89 | + """ |
| 90 | + docs_parts = self.doc_name.split("/")[:-1] |
| 91 | + |
| 92 | + breadcrumb_parts = [ |
| 93 | + self.env.titles[part].astext() for part in docs_parts if part in self.env.titles |
| 94 | + ] |
| 95 | + |
| 96 | + # Create breadcrumb hierarchy |
| 97 | + breadcrumbs = " > ".join(breadcrumb_parts) |
| 98 | + ignore_doc_title = section_title == self.doc_title |
| 99 | + |
| 100 | + # Construct final breadcrumb path |
| 101 | + if ignore_doc_title: |
| 102 | + return f"{breadcrumbs} > {section_title}" if breadcrumbs else section_title |
| 103 | + |
| 104 | + return ( |
| 105 | + f"{breadcrumbs} > {self.doc_title} > {section_title}" |
| 106 | + if breadcrumbs |
| 107 | + else f"{self.doc_title} > {section_title}" |
| 108 | + ) |
| 109 | + |
70 | 110 | @property
|
71 | 111 | def indices(self):
|
72 |
| - """Get search index.""" |
73 |
| - for sections in self.sections: |
74 |
| - search_index = { |
75 |
| - "objectID": self._doc_name, |
76 |
| - "href": f"{self.doc_path}#{sections['section_anchor_id']}", |
77 |
| - "title": self.doc_title, |
78 |
| - "section": sections["section_title"], |
79 |
| - "text": sections["section_text"], |
| 112 | + """Generate indices for each section.""" |
| 113 | + for section in self.sections: |
| 114 | + breadcrumbs = self.generate_breadcrumbs(section["title"]) |
| 115 | + yield { |
| 116 | + "objectID": self.doc_name, |
| 117 | + "href": f"{self.doc_path}#{section['anchor_id']}", |
| 118 | + "title": breadcrumbs, |
| 119 | + "section": section["title"], |
| 120 | + "text": section["text"], |
80 | 121 | }
|
81 |
| - yield search_index |
82 | 122 |
|
83 | 123 |
|
84 | 124 | def _title_to_anchor(title: str) -> str:
|
85 |
| - """Convert title to anchor.""" |
| 125 | + """Convert a title to a URL-friendly anchor identifier.""" |
86 | 126 | return re.sub(r"[^\w\s-]", "", title.lower().strip().replace(" ", "-"))
|
87 | 127 |
|
88 | 128 |
|
89 | 129 | def create_search_index(app, exception):
|
90 |
| - """Create search index for the document in build finished. |
| 130 | + """ |
| 131 | + Generate search index at the end of the Sphinx build process. |
91 | 132 |
|
92 | 133 | Parameters
|
93 | 134 | ----------
|
94 | 135 | app : Sphinx
|
95 |
| - Sphinx application. |
96 |
| - exception : Any |
97 |
| - Exception. |
| 136 | + Sphinx application instance. |
| 137 | + exception : Exception |
| 138 | + Exception raised during the build process, if any. |
98 | 139 | """
|
99 | 140 | if exception:
|
100 | 141 | return
|
101 | 142 |
|
102 |
| - all_docs = app.env.found_docs |
103 | 143 | search_index_list = []
|
104 | 144 |
|
105 |
| - for document in all_docs: |
| 145 | + for document in app.env.found_docs: |
106 | 146 | search_index = SearchIndex(document, app)
|
107 |
| - search_index.iterate_through_docs() |
| 147 | + search_index.build_sections() |
108 | 148 | search_index_list.extend(search_index.indices)
|
109 | 149 |
|
110 |
| - search_index = app.builder.outdir / "_static" / "search.json" |
111 |
| - with search_index.open("w", encoding="utf-8") as index_file: |
| 150 | + search_index_path = app.builder.outdir / "_static" / "search.json" |
| 151 | + with search_index_path.open("w", encoding="utf-8") as index_file: |
112 | 152 | json.dump(search_index_list, index_file, ensure_ascii=False, indent=4)
|
0 commit comments