88:license: BSD, see LICENSE for details.
99"""
1010
11- from typing import Iterable
11+ from typing import Iterable , TypeVar
1212import re
1313
14- from sphinx .domains import Index , IndexEntry
14+ from sphinx .domains import Domain , Index , IndexEntry
1515from sphinx .util import logging
1616from docutils import core , nodes
1717from docutils .parsers .rst import roles
1818
19- from .schema import Schema
19+ from .schema import Schema , Value , Classifier , Classif
2020
2121logger = logging .getLogger (__name__ )
2222
@@ -26,20 +26,20 @@ class AnyIndex(Index):
2626 Index subclass to provide the object reference index.
2727 """
2828
29+ domain : Domain # for type hint
2930 schema : Schema
30- # TODO: document
3131 field : str | None = None
32-
33- name : str
34- localname : str
35- shortname : str
32+ classifier : Classifier
3633
3734 @classmethod
38- def derive (cls , schema : Schema , field : str | None = None ) -> type ['AnyIndex' ]:
35+ def derive (
36+ cls , schema : Schema , field : str | None , classifier : Classifier
37+ ) -> type ['AnyIndex' ]:
3938 """Generate an AnyIndex child class for indexing object."""
39+ # TODO: add Indexer.name
4040 if field :
4141 typ = f'Any{ schema .objtype .title ()} { field .title ()} Index'
42- name = schema .objtype + '.' + field
42+ name = schema .objtype + '.' + field # TOOD: objtype_and_objfield_to_reftype
4343 localname = f'{ schema .objtype .title ()} { field .title ()} Reference Index'
4444 else :
4545 typ = f'Any{ schema .objtype .title ()} Index'
@@ -49,65 +49,116 @@ def derive(cls, schema: Schema, field: str | None = None) -> type['AnyIndex']:
4949 typ ,
5050 (cls ,),
5151 {
52- 'schema' : schema ,
53- 'field' : field ,
5452 'name' : name ,
5553 'localname' : localname ,
5654 'shortname' : 'references' ,
55+ 'schema' : schema ,
56+ 'field' : field ,
57+ 'classifier' : classifier ,
5758 },
5859 )
5960
6061 def generate (
6162 self , docnames : Iterable [str ] | None = None
6263 ) -> tuple [list [tuple [str , list [IndexEntry ]]], bool ]:
6364 """Override parent method."""
64- content = {} # type: dict[str, list[IndexEntry]]
65- # list of all references
66- objrefs = sorted (self .domain .data ['references' ].items ())
6765
68- # Reference value -> object IDs
69- objs_with_same_ref : dict [str , set [str ]] = {}
66+ # Single index for generating normal entries (subtype=0).
67+ # Category (lv1) → Category (for ordering objids) → objids
68+ singleidx : dict [Classif , dict [Classif , set [str ]]] = {}
69+ # Dual index for generating entrie (subtype=1) and its sub-entries (subtype=2).
70+ # Category (lv1) → Category (lv2) → Category (for ordering objids) → objids
71+ dualidx : dict [Classif , dict [Classif , dict [Classif , set [str ]]]] = {}
7072
73+ objrefs = sorted (self .domain .data ['references' ].items ())
7174 for (objtype , objfield , objref ), objids in objrefs :
7275 if objtype != self .schema .objtype :
7376 continue
7477 if self .field and objfield != self .field :
7578 continue
76- objs = objs_with_same_ref .setdefault (objref , set ())
77- objs .update (objids )
78-
79- for objref , objids in sorted (objs_with_same_ref .items ()):
80- # Add a entry for objref
81- # 1: Entry with sub-entries.
82- entries = content .setdefault (objref , [])
83- for objid in sorted (objids ):
84- docname , anchor , obj = self .domain .data ['objects' ][
85- self .schema .objtype , objid
86- ]
87- if docnames and docname not in docnames :
88- continue
89- name = self .schema .title_of (obj ) or objid
90- extra = '' if name == objid else objid
91- objcont = self .schema .content_of (obj )
92- if isinstance (objcont , str ):
93- desc = objcont
94- elif isinstance (objcont , list ):
95- desc = '\n ' .join (objcont )
79+
80+ # TODO: pass a real value
81+ for catelog in self .classifier .classify (Value (objref )):
82+ category = catelog .as_category ()
83+ entry = catelog .as_entry ()
84+ if entry is None :
85+ singleidx .setdefault (category , {}).setdefault (
86+ catelog , set ()
87+ ).update (objids )
9688 else :
97- desc = ''
98- desc = strip_rst_markups (desc ) # strip rst markups
99- desc = '' .join (
100- [ln for ln in desc .split ('\n ' ) if ln .strip ()]
101- ) # strip NEWLINE
102- desc = desc [:50 ] + '…' if len (desc ) > 50 else desc # shorten
103- # 0: Normal entry
104- entries .append (IndexEntry (name , 0 , docname , anchor , extra , '' , desc ))
105-
106- # sort by first letter
107- sorted_content = sorted (content .items ())
89+ dualidx .setdefault (category , {}).setdefault (entry , {}).setdefault (
90+ catelog , set ()
91+ ).update (objids )
92+
93+ content : dict [Classif , list [IndexEntry ]] = {} # category → entries
94+ for category , entries in self ._sort_by_catelog (singleidx ):
95+ index_entries = content .setdefault (category , [])
96+ for category , objids in self ._sort_by_catelog (entries ):
97+ for objid in objids :
98+ entry = self ._generate_index_entry (objid , docnames , category )
99+ if entry is None :
100+ continue
101+ index_entries .append (entry )
102+
103+ for category , entries in self ._sort_by_catelog (dualidx ):
104+ index_entries = content .setdefault (category , [])
105+ for entry , subentries in self ._sort_by_catelog (entries ):
106+ index_entries .append (self ._generate_empty_index_entry (entry ))
107+ for subentry , objids in self ._sort_by_catelog (subentries ):
108+ for objid in objids :
109+ entry = self ._generate_index_entry (objid , docnames , subentry )
110+ if entry is None :
111+ continue
112+ index_entries .append (entry )
113+
114+ # sort by category, and map classif -> str
115+ sorted_content = [
116+ (classif .leaf , entries )
117+ for classif , entries in self ._sort_by_catelog (content )
118+ ]
108119
109120 return sorted_content , False
110121
122+ def _generate_index_entry (
123+ self , objid : str , ignore_docnames : Iterable [str ] | None , category : Classif
124+ ) -> IndexEntry | None :
125+ docname , anchor , obj = self .domain .data ['objects' ][self .schema .objtype , objid ]
126+ if ignore_docnames and docname not in ignore_docnames :
127+ return None
128+ name = self .schema .title_of (obj ) or objid
129+ subtype = category .index_entry_subtype
130+ extra = category .leaf
131+ objcont = self .schema .content_of (obj )
132+ if isinstance (objcont , str ):
133+ desc = objcont
134+ elif isinstance (objcont , list ):
135+ desc = '\n ' .join (objcont ) # FIXME: use schema.Form
136+ else :
137+ desc = ''
138+ desc = strip_rst_markups (desc ) # strip rst markups
139+ desc = '' .join ([ln for ln in desc .split ('\n ' ) if ln .strip ()]) # strip NEWLINE
140+ desc = desc [:50 ] + '…' if len (desc ) > 50 else desc # shorten
141+ return IndexEntry (
142+ name , # the name of the index entry to be displayed
143+ subtype , # the sub-entry related type
144+ docname , # docname where the entry is located
145+ anchor , # anchor for the entry within docname
146+ extra , # extra info for the entry
147+ '' , # qualifier for the description
148+ desc , # description for the entry
149+ )
150+
151+ def _generate_empty_index_entry (self , category : Classif ) -> IndexEntry :
152+ return IndexEntry (
153+ category .leaf , category .index_entry_subtype , '' , '' , '' , '' , ''
154+ )
155+
156+ _T = TypeVar ('_T' )
157+
158+ def _sort_by_catelog (self , d : dict [Classif , _T ]) -> list [tuple [Classif , _T ]]:
159+ """Helper for sorting dict items by Category."""
160+ return self .classifier .sort (d .items (), lambda x : x [0 ])
161+
111162
112163def strip_rst_markups (rst : str ) -> str :
113164 """Strip markups and newlines in rST.
0 commit comments