"""Index entries adapters for sphinx.environment. """ import re import unicodedata from itertools import groupby from typing import Any, Dict, List, Pattern, Tuple, cast from sphinx.builders import Builder from sphinx.domains.index import IndexDomain from sphinx.environment import BuildEnvironment from sphinx.errors import NoUri from sphinx.locale import _, __ from sphinx.util import logging, split_into logger = logging.getLogger(__name__) class IndexEntries: def __init__(self, env: BuildEnvironment) -> None: self.env = env def create_index(self, builder: Builder, group_entries: bool = True, _fixre: Pattern = re.compile(r'(.*) ([(][^()]*[)])') ) -> List[Tuple[str, List[Tuple[str, Any]]]]: """Create the real index from the collected index entries.""" new: Dict[str, List] = {} def add_entry(word: str, subword: str, main: str, link: bool = True, dic: Dict = new, key: str = None) -> None: # Force the word to be unicode if it's a ASCII bytestring. # This will solve problems with unicode normalization later. # For instance the RFC role will add bytestrings at the moment word = str(word) entry = dic.get(word) if not entry: dic[word] = entry = [[], {}, key] if subword: add_entry(subword, '', main, link=link, dic=entry[1], key=key) elif link: try: uri = builder.get_relative_uri('genindex', fn) + '#' + tid except NoUri: pass else: entry[0].append((main, uri)) domain = cast(IndexDomain, self.env.get_domain('index')) for fn, entries in domain.entries.items(): # new entry types must be listed in directives/other.py! for type, value, tid, main, index_key in entries: # noqa: B007 try: if type == 'single': try: entry, subentry = split_into(2, 'single', value) except ValueError: entry, = split_into(1, 'single', value) subentry = '' add_entry(entry, subentry, main, key=index_key) elif type == 'pair': first, second = split_into(2, 'pair', value) add_entry(first, second, main, key=index_key) add_entry(second, first, main, key=index_key) elif type == 'triple': first, second, third = split_into(3, 'triple', value) add_entry(first, second + ' ' + third, main, key=index_key) add_entry(second, third + ', ' + first, main, key=index_key) add_entry(third, first + ' ' + second, main, key=index_key) elif type == 'see': first, second = split_into(2, 'see', value) add_entry(first, _('see %s') % second, None, link=False, key=index_key) elif type == 'seealso': first, second = split_into(2, 'see', value) add_entry(first, _('see also %s') % second, None, link=False, key=index_key) else: logger.warning(__('unknown index entry type %r'), type, location=fn) except ValueError as err: logger.warning(str(err), location=fn) # sort the index entries for same keyword. def keyfunc0(entry: Tuple[str, str]) -> Tuple[bool, str]: main, uri = entry return (not main, uri) # show main entries at first for indexentry in new.values(): indexentry[0].sort(key=keyfunc0) for subentry in indexentry[1].values(): subentry[0].sort(key=keyfunc0) # type: ignore # sort the index entries def keyfunc(entry: Tuple[str, List]) -> Tuple[Tuple[int, str], str]: key, (void, void, category_key) = entry if category_key: # using specified category key to sort key = category_key lckey = unicodedata.normalize('NFD', key.lower()) if lckey.startswith('\N{RIGHT-TO-LEFT MARK}'): lckey = lckey[1:] if lckey[0:1].isalpha() or lckey.startswith('_'): # put non-symbol characters at the following group (1) sortkey = (1, lckey) else: # put symbols at the front of the index (0) sortkey = (0, lckey) # ensure a deterministic order *within* letters by also sorting on # the entry itself return (sortkey, entry[0]) newlist = sorted(new.items(), key=keyfunc) if group_entries: # fixup entries: transform # func() (in module foo) # func() (in module bar) # into # func() # (in module foo) # (in module bar) oldkey = '' oldsubitems: Dict[str, List] = None i = 0 while i < len(newlist): key, (targets, subitems, _key) = newlist[i] # cannot move if it has subitems; structure gets too complex if not subitems: m = _fixre.match(key) if m: if oldkey == m.group(1): # prefixes match: add entry as subitem of the # previous entry oldsubitems.setdefault(m.group(2), [[], {}, _key])[0].\ extend(targets) del newlist[i] continue oldkey = m.group(1) else: oldkey = key oldsubitems = subitems i += 1 # sort the sub-index entries def keyfunc2(entry: Tuple[str, List]) -> str: key = unicodedata.normalize('NFD', entry[0].lower()) if key.startswith('\N{RIGHT-TO-LEFT MARK}'): key = key[1:] if key[0:1].isalpha() or key.startswith('_'): key = chr(127) + key return key # group the entries by letter def keyfunc3(item: Tuple[str, List]) -> str: # hack: mutating the subitems dicts to a list in the keyfunc k, v = item v[1] = sorted(((si, se) for (si, (se, void, void)) in v[1].items()), key=keyfunc2) if v[2] is None: # now calculate the key if k.startswith('\N{RIGHT-TO-LEFT MARK}'): k = k[1:] letter = unicodedata.normalize('NFD', k[0])[0].upper() if letter.isalpha() or letter == '_': return letter else: # get all other symbols under one heading return _('Symbols') else: return v[2] return [(key_, list(group)) for (key_, group) in groupby(newlist, keyfunc3)]