mirror of
https://github.com/sphinx-doc/sphinx.git
synced 2025-02-25 18:55:22 -06:00
Change domain-index API: introduce a class.
This commit is contained in:
@@ -47,6 +47,7 @@ latex_elements = {
|
||||
'fontpkg': '\\usepackage{palatino}',
|
||||
}
|
||||
|
||||
autodoc_member_order = 'groupwise'
|
||||
todo_include_todos = True
|
||||
|
||||
man_pages = [
|
||||
|
||||
@@ -147,9 +147,9 @@ General configuration
|
||||
|
||||
Directories in which to search for additional Sphinx message catalogs (see
|
||||
:confval:`language`), relative to the source directory. The directories on
|
||||
this path are searched by the standard :mod:`gettext` module for a domain of
|
||||
``sphinx``; so if you add the directory :file:`./locale` to this settting,
|
||||
the message catalogs must be in
|
||||
this path are searched by the standard :mod:`gettext` module for a text
|
||||
domain of ``sphinx``; so if you add the directory :file:`./locale` to this
|
||||
settting, the message catalogs must be in
|
||||
:file:`./locale/{language}/LC_MESSAGES/sphinx.mo`.
|
||||
|
||||
The default is ``[]``.
|
||||
@@ -188,8 +188,8 @@ General configuration
|
||||
|
||||
The name of a reST role (builtin or Sphinx extension) to use as the default
|
||||
role, that is, for text marked up ```like this```. This can be set to
|
||||
``'obj'`` to make ```filter``` a cross-reference to the function "filter".
|
||||
The default is ``None``, which doesn't reassign the default role.
|
||||
``'py:obj'`` to make ```filter``` a cross-reference to the Python function
|
||||
"filter". The default is ``None``, which doesn't reassign the default role.
|
||||
|
||||
The default role can always be set within individual documents using the
|
||||
standard reST :dir:`default-role` directive.
|
||||
|
||||
@@ -46,17 +46,22 @@ the following public API:
|
||||
.. method:: Sphinx.add_domain(domain)
|
||||
|
||||
Make the given *domain* (which must be a class; more precisely, a subclass of
|
||||
:class:`sphinx.domains.Domain`) known to Sphinx.
|
||||
|
||||
.. XXX where is Domain documented?
|
||||
:class:`~sphinx.domains.Domain`) known to Sphinx.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
.. method:: Sphinx.override_domain(domain)
|
||||
|
||||
Make the given *domain* known to Sphinx, assuming that there is already a
|
||||
domain with its ``.name``. The new domain must be a subclass of the existing
|
||||
one.
|
||||
Make the given *domain* class known to Sphinx, assuming that there is already
|
||||
a domain with its ``.name``. The new domain must be a subclass of the
|
||||
existing one.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
.. method:: Sphinx.add_index_to_domain(domain, index)
|
||||
|
||||
Add a custom *index* class to the domain named *domain*. *index* must be a
|
||||
subclass of :class:`~sphinx.domains.Index`.
|
||||
|
||||
.. versionadded:: 1.0
|
||||
|
||||
@@ -445,3 +450,19 @@ The template bridge
|
||||
|
||||
.. autoclass:: TemplateBridge
|
||||
:members:
|
||||
|
||||
|
||||
.. _domain-api:
|
||||
|
||||
Domain API
|
||||
----------
|
||||
|
||||
.. module:: sphinx.domains
|
||||
|
||||
.. autoclass:: Domain
|
||||
:members:
|
||||
|
||||
.. autoclass:: ObjType
|
||||
|
||||
.. autoclass:: Index
|
||||
:members:
|
||||
|
||||
@@ -417,12 +417,11 @@ class Sphinx(object):
|
||||
raise ExtensionError('domain %s not yet registered' % domain)
|
||||
self.domains[domain].roles[name] = role
|
||||
|
||||
# XXX needs documentation
|
||||
def add_index_to_domain(self, domain, name, localname, shortname, func):
|
||||
if domain not in self.domains:
|
||||
raise ExtensionError('domain %s not yet registered' % domain)
|
||||
self.domains[domain].indices.append((name, longname, shortname))
|
||||
setattr(self.domains[domain], '_get_%s_index' % name, func)
|
||||
self.domains[domain].indices.append((name, localname, shortname))
|
||||
setattr(self.domains[domain], 'get_%s_index' % name, func)
|
||||
|
||||
def add_object_type(self, directivename, rolename, indextemplate='',
|
||||
parse_node=None, ref_nodeclass=None, objname=''):
|
||||
|
||||
@@ -236,8 +236,8 @@ class StandaloneHTMLBuilder(Builder):
|
||||
indices_config = self.config.html_domain_indices
|
||||
if indices_config:
|
||||
for domain in self.env.domains.itervalues():
|
||||
for indexinfo in domain.indices:
|
||||
indexname = '%s-%s' % (domain.name, indexinfo[0])
|
||||
for indexcls in domain.indices:
|
||||
indexname = '%s-%s' % (domain.name, indexcls.name)
|
||||
if isinstance(indices_config, list):
|
||||
if indexname not in indices_config:
|
||||
continue
|
||||
@@ -245,8 +245,10 @@ class StandaloneHTMLBuilder(Builder):
|
||||
if indexname == 'py-modindex' and \
|
||||
not self.config.html_use_modindex:
|
||||
continue
|
||||
if domain.has_index_entries(indexinfo[0]):
|
||||
self.domain_indices.append((domain.name,) + indexinfo)
|
||||
content, collapse = indexcls(domain).generate()
|
||||
if content:
|
||||
self.domain_indices.append(
|
||||
(indexname, indexcls, content, collapse))
|
||||
|
||||
# format the "last updated on" string, only once is enough since it
|
||||
# typically doesn't include the time of day
|
||||
@@ -272,9 +274,11 @@ class StandaloneHTMLBuilder(Builder):
|
||||
rellinks = []
|
||||
if self.config.html_use_index:
|
||||
rellinks.append(('genindex', _('General Index'), 'I', _('index')))
|
||||
for index in self.domain_indices:
|
||||
if index[3]:
|
||||
rellinks.append(('%s-%s' % index[0:2], index[2], '', index[3]))
|
||||
for indexname, indexcls, content, collapse in self.domain_indices:
|
||||
# if it has a short name
|
||||
if indexcls.shortname:
|
||||
rellinks.append((indexname, indexcls.localname,
|
||||
'', indexcls.shortname))
|
||||
|
||||
if self.config.html_style is not None:
|
||||
stylename = self.config.html_style
|
||||
@@ -470,14 +474,12 @@ class StandaloneHTMLBuilder(Builder):
|
||||
self.handle_page('genindex', genindexcontext, 'genindex.html')
|
||||
|
||||
def write_domain_indices(self):
|
||||
for index in self.domain_indices:
|
||||
content, collapse = self.env.domains[index[0]].get_index(index[1])
|
||||
for indexname, indexcls, content, collapse in self.domain_indices:
|
||||
indexcontext = dict(
|
||||
indextitle = index[2],
|
||||
indextitle = indexcls.localname,
|
||||
content = content,
|
||||
collapse_index = collapse,
|
||||
)
|
||||
indexname = '%s-%s' % index[0:2]
|
||||
self.info(' ' + indexname, nonl=1)
|
||||
self.handle_page(indexname, indexcontext, 'domainindex.html')
|
||||
|
||||
|
||||
@@ -10,6 +10,8 @@
|
||||
:license: BSD, see LICENSE for details.
|
||||
"""
|
||||
|
||||
from sphinx.errors import SphinxError
|
||||
|
||||
|
||||
class ObjType(object):
|
||||
"""
|
||||
@@ -23,7 +25,7 @@ class ObjType(object):
|
||||
- *roles*: all the roles that can refer to an object of this type
|
||||
- *attrs*: object attributes -- currently only "searchprio" is known,
|
||||
which defines the object's priority in the full-text search index,
|
||||
see `Domain.get_objects`.
|
||||
see :meth:`Domain.get_objects()`.
|
||||
"""
|
||||
|
||||
known_attrs = {
|
||||
@@ -37,6 +39,63 @@ class ObjType(object):
|
||||
self.attrs.update(attrs)
|
||||
|
||||
|
||||
class Index(object):
|
||||
"""
|
||||
An Index is the description for a domain-specific index. To add an index to
|
||||
a domain, subclass Index, overriding the three name attributes:
|
||||
|
||||
* `name` is an identifier used for generating file names.
|
||||
* `localname` is the section title for the index.
|
||||
* `shortname` is a short name for the index, for use in the relation bar in
|
||||
HTML output. Can be empty to disable entries in the relation bar.
|
||||
|
||||
and providing a :meth:`generate()` method. Then, add the index class to
|
||||
your domain's `indices` list. Extensions can add indices to existing
|
||||
domains using :meth:`~sphinx.application.Sphinx.add_index_to_domain()`.
|
||||
"""
|
||||
|
||||
name = None
|
||||
localname = None
|
||||
shortname = None
|
||||
|
||||
def __init__(self, domain):
|
||||
if self.name is None or self.localname is None:
|
||||
raise SphinxError('Index subclass %s has no valid name or localname'
|
||||
% self.__class__.__name__)
|
||||
self.domain = domain
|
||||
|
||||
def generate(self, docnames=None):
|
||||
"""
|
||||
Return entries for the index given by *name*. If *docnames* is given,
|
||||
restrict to entries referring to these docnames.
|
||||
|
||||
The return value is a tuple of ``(content, collapse)``, where *collapse*
|
||||
is a boolean that determines if sub-entries should start collapsed (for
|
||||
output formats that support collapsing sub-entries).
|
||||
|
||||
*content* is a sequence of ``(letter, entries)`` tuples, where *letter*
|
||||
is the "heading" for the given *entries*, usually the starting letter.
|
||||
|
||||
*entries* is a sequence of single entries, where a single entry is a
|
||||
sequence ``[name, subtype, docname, anchor, extra, qualifier, descr]``.
|
||||
The items in this sequence have the following meaning:
|
||||
|
||||
- `name` -- the name of the index entry to be displayed
|
||||
- `subtype` -- sub-entry related type:
|
||||
0 -- normal entry
|
||||
1 -- entry with sub-entries
|
||||
2 -- sub-entry
|
||||
- `docname` -- docname where the entry is located
|
||||
- `anchor` -- anchor for the entry within `docname`
|
||||
- `extra` -- extra info for the entry
|
||||
- `qualifier` -- qualifier for the description
|
||||
- `descr` -- description for the entry
|
||||
|
||||
Qualifier and description are not rendered e.g. in LaTeX output.
|
||||
"""
|
||||
return []
|
||||
|
||||
|
||||
class Domain(object):
|
||||
"""
|
||||
A Domain is meant to be a group of "object" description directives for
|
||||
@@ -45,9 +104,9 @@ class Domain(object):
|
||||
of a templating language, Sphinx roles and directives, etc.
|
||||
|
||||
Each domain has a separate storage for information about existing objects
|
||||
and how to reference them in `data`, which must be a dictionary. It also
|
||||
must implement several functions that expose the object information in a
|
||||
uniform way to parts of Sphinx that allow the user to reference or search
|
||||
and how to reference them in `self.data`, which must be a dictionary. It
|
||||
also must implement several functions that expose the object information in
|
||||
a uniform way to parts of Sphinx that allow the user to reference or search
|
||||
for objects in a domain-agnostic way.
|
||||
|
||||
About `self.data`: since all object and cross-referencing information is
|
||||
@@ -56,8 +115,8 @@ class Domain(object):
|
||||
build process starts, every active domain is instantiated and given the
|
||||
environment object; the `domaindata` dict must then either be nonexistent or
|
||||
a dictionary whose 'version' key is equal to the domain class'
|
||||
`data_version` attribute. Otherwise, `IOError` is raised and the pickled
|
||||
environment is discarded.
|
||||
:attr:`data_version` attribute. Otherwise, `IOError` is raised and the
|
||||
pickled environment is discarded.
|
||||
"""
|
||||
|
||||
#: domain name: should be short, but unique
|
||||
@@ -70,12 +129,12 @@ class Domain(object):
|
||||
directives = {}
|
||||
#: role name -> role callable
|
||||
roles = {}
|
||||
#: (index identifier, localized index name, localized short name) tuples
|
||||
#: a list of Index subclasses
|
||||
indices = []
|
||||
|
||||
#: data value for a fresh environment
|
||||
initial_data = {}
|
||||
#: data version
|
||||
#: data version, bump this when the format of `self.data` changes
|
||||
data_version = 0
|
||||
|
||||
def __init__(self, env):
|
||||
@@ -153,8 +212,8 @@ class Domain(object):
|
||||
then given to the 'missing-reference' event, and if that yields no
|
||||
resolution, replaced by *contnode*.
|
||||
|
||||
The method can also raise `sphinx.environment.NoUri` to suppress the
|
||||
'missing-reference' event being emitted.
|
||||
The method can also raise :exc:`sphinx.environment.NoUri` to suppress
|
||||
the 'missing-reference' event being emitted.
|
||||
"""
|
||||
pass
|
||||
|
||||
@@ -170,63 +229,13 @@ class Domain(object):
|
||||
* `priority` -- how "important" the object is (determines placement
|
||||
in search results)
|
||||
|
||||
1: default priority (placed before full-text matches)
|
||||
0: object is important (placed before default-priority objects)
|
||||
2: object is unimportant (placed after full-text matches)
|
||||
-1: object should not show up in search at all
|
||||
- 1: default priority (placed before full-text matches)
|
||||
- 0: object is important (placed before default-priority objects)
|
||||
- 2: object is unimportant (placed after full-text matches)
|
||||
- -1: object should not show up in search at all
|
||||
"""
|
||||
return []
|
||||
|
||||
def has_index_entries(self, name, docnames=None):
|
||||
"""
|
||||
Return True if there are entries for the index given by *name*. If
|
||||
*docnames* is given, restrict to entries referring to these docnames.
|
||||
|
||||
Do not overwrite this method, add a method ``has_<name>_entries(self,
|
||||
docnames=None)`` method for every index.
|
||||
"""
|
||||
func = getattr(self, 'has_%s_entries' % name, None)
|
||||
if not func:
|
||||
return bool(self.get_index(name, docnames))
|
||||
return func(docnames)
|
||||
|
||||
def get_index(self, name, docnames=None):
|
||||
"""
|
||||
Return entries for the index given by *name*. If *docnames* is given,
|
||||
restrict to entries referring to these docnames.
|
||||
|
||||
The return value is a tuple of ``(content, collapse)``, where *collapse*
|
||||
is a boolean that determines if sub-entries should start collapsed (for
|
||||
output formats that support collapsing sub-entries).
|
||||
|
||||
*content* is a sequence of ``(letter, entries)`` tuples, where *letter*
|
||||
is the "heading" for the given *entries*, usually the starting letter.
|
||||
|
||||
*entries* is a sequence of single entries, where a single entry is a
|
||||
sequence ``[name, subtype, docname, anchor, extra, qualifier, descr]``.
|
||||
The items in this sequence have the following meaning:
|
||||
|
||||
- `name` -- the name of the index entry to be displayed
|
||||
- `subtype` -- sub-entry related type:
|
||||
0 -- normal entry
|
||||
1 -- entry with sub-entries
|
||||
2 -- sub-entry
|
||||
- `docname` -- docname where the entry is located
|
||||
- `anchor` -- anchor for the entry within `docname`
|
||||
- `extra` -- extra info for the entry
|
||||
- `qualifier` -- qualifier for the description
|
||||
- `descr` -- description for the entry
|
||||
|
||||
Qualifier and description are not rendered e.g. in LaTeX output.
|
||||
|
||||
Do not overwrite this method, add a method ``get_<name>_index(self,
|
||||
docnames=None)`` method for every index.
|
||||
"""
|
||||
func = getattr(self, 'get_%s_index' % name, None)
|
||||
if not func:
|
||||
return []
|
||||
return func(docnames)
|
||||
|
||||
|
||||
from sphinx.domains.c import CDomain
|
||||
from sphinx.domains.std import StandardDomain
|
||||
|
||||
@@ -17,7 +17,7 @@ from docutils.parsers.rst import directives
|
||||
from sphinx import addnodes
|
||||
from sphinx.roles import XRefRole
|
||||
from sphinx.locale import l_, _
|
||||
from sphinx.domains import Domain, ObjType
|
||||
from sphinx.domains import Domain, ObjType, Index
|
||||
from sphinx.directives import ObjectDescription
|
||||
from sphinx.util.nodes import make_refnode
|
||||
from sphinx.util.compat import Directive
|
||||
@@ -419,6 +419,75 @@ class PyXRefRole(XRefRole):
|
||||
return title, target
|
||||
|
||||
|
||||
class PythonModuleIndex(Index):
|
||||
"""
|
||||
Index subclass to provide the Python module index.
|
||||
"""
|
||||
|
||||
name = 'modindex'
|
||||
localname = l_('Python Module Index')
|
||||
shortname = l_('modules')
|
||||
|
||||
def generate(self, docnames=None):
|
||||
content = {}
|
||||
# list of prefixes to ignore
|
||||
ignores = self.domain.env.config['modindex_common_prefix']
|
||||
ignores = sorted(ignores, key=len, reverse=True)
|
||||
# list of all modules, sorted by module name
|
||||
modules = sorted(self.domain.data['modules'].iteritems(),
|
||||
key=lambda x: x[0].lower())
|
||||
# sort out collapsable modules
|
||||
prev_modname = ''
|
||||
num_toplevels = 0
|
||||
for modname, (docname, synopsis, platforms, deprecated) in modules:
|
||||
if docnames and docname not in docnames:
|
||||
continue
|
||||
|
||||
for ignore in ignores:
|
||||
if modname.startswith(ignore):
|
||||
modname = modname[len(ignore):]
|
||||
stripped = ignore
|
||||
break
|
||||
else:
|
||||
stripped = ''
|
||||
|
||||
# we stripped the whole module name?
|
||||
if not modname:
|
||||
modname, stripped = stripped, ''
|
||||
|
||||
entries = content.setdefault(modname[0].lower(), [])
|
||||
|
||||
package = modname.split('.')[0]
|
||||
if package != modname:
|
||||
# it's a submodule
|
||||
if prev_modname == package:
|
||||
# first submodule - make parent a group head
|
||||
entries[-1][1] = 1
|
||||
elif not prev_modname.startswith(package):
|
||||
# submodule without parent in list, add dummy entry
|
||||
entries.append([stripped + package, 1, '', '', '', '', ''])
|
||||
subtype = 2
|
||||
else:
|
||||
num_toplevels += 1
|
||||
subtype = 0
|
||||
|
||||
qualifier = deprecated and _('Deprecated') or ''
|
||||
entries.append([stripped + modname, subtype, docname,
|
||||
'module-' + stripped + modname, platforms,
|
||||
qualifier, synopsis])
|
||||
prev_modname = modname
|
||||
|
||||
# apply heuristics when to collapse modindex at page load:
|
||||
# only collapse if number of toplevel modules is larger than
|
||||
# number of submodules
|
||||
collapse = len(modules) - num_toplevels < num_toplevels
|
||||
|
||||
# sort by first letter
|
||||
content = sorted(content.iteritems())
|
||||
|
||||
return content, collapse
|
||||
|
||||
|
||||
class PythonDomain(Domain):
|
||||
"""Python language domain."""
|
||||
name = 'py'
|
||||
@@ -463,7 +532,7 @@ class PythonDomain(Domain):
|
||||
'modules': {}, # modname -> docname, synopsis, platform, deprecated
|
||||
}
|
||||
indices = [
|
||||
('modindex', l_('Python Module Index'), l_('modules')),
|
||||
PythonModuleIndex,
|
||||
]
|
||||
|
||||
def clear_doc(self, docname):
|
||||
@@ -548,71 +617,3 @@ class PythonDomain(Domain):
|
||||
yield (modname, 'module', info[0], 'module-' + modname, 0)
|
||||
for refname, (docname, type) in self.data['objects'].iteritems():
|
||||
yield (refname, type, docname, refname, 1)
|
||||
|
||||
def has_modindex_entries(self, docnames=None):
|
||||
if not docnames:
|
||||
return bool(self.data['modules'])
|
||||
else:
|
||||
for modname, info in self.data['modules'].iteritems():
|
||||
if info[0] in docnames:
|
||||
return True
|
||||
return False
|
||||
|
||||
def get_modindex_index(self, docnames=None):
|
||||
content = {}
|
||||
# list of prefixes to ignore
|
||||
ignores = self.env.config['modindex_common_prefix']
|
||||
ignores = sorted(ignores, key=len, reverse=True)
|
||||
# list of all modules, sorted by module name
|
||||
modules = sorted(self.data['modules'].iteritems(),
|
||||
key=lambda x: x[0].lower())
|
||||
# sort out collapsable modules
|
||||
prev_modname = ''
|
||||
num_toplevels = 0
|
||||
for modname, (docname, synopsis, platforms, deprecated) in modules:
|
||||
if docnames and docname not in docnames:
|
||||
continue
|
||||
|
||||
for ignore in ignores:
|
||||
if modname.startswith(ignore):
|
||||
modname = modname[len(ignore):]
|
||||
stripped = ignore
|
||||
break
|
||||
else:
|
||||
stripped = ''
|
||||
|
||||
# we stripped the whole module name?
|
||||
if not modname:
|
||||
modname, stripped = stripped, ''
|
||||
|
||||
entries = content.setdefault(modname[0].lower(), [])
|
||||
|
||||
package = modname.split('.')[0]
|
||||
if package != modname:
|
||||
# it's a submodule
|
||||
if prev_modname == package:
|
||||
# first submodule - make parent a group head
|
||||
entries[-1][1] = 1
|
||||
elif not prev_modname.startswith(package):
|
||||
# submodule without parent in list, add dummy entry
|
||||
entries.append([stripped + package, 1, '', '', '', '', ''])
|
||||
subtype = 2
|
||||
else:
|
||||
num_toplevels += 1
|
||||
subtype = 0
|
||||
|
||||
qualifier = deprecated and _('Deprecated') or ''
|
||||
entries.append([stripped + modname, subtype, docname,
|
||||
'module-' + stripped + modname, platforms,
|
||||
qualifier, synopsis])
|
||||
prev_modname = modname
|
||||
|
||||
# apply heuristics when to collapse modindex at page load:
|
||||
# only collapse if number of toplevel modules is larger than
|
||||
# number of submodules
|
||||
collapse = len(modules) - num_toplevels < num_toplevels
|
||||
|
||||
# sort by first letter
|
||||
content = sorted(content.iteritems())
|
||||
|
||||
return content, collapse
|
||||
|
||||
@@ -276,8 +276,8 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
indices_config = self.builder.config.latex_domain_indices
|
||||
if indices_config:
|
||||
for domain in self.builder.env.domains.itervalues():
|
||||
for indexinfo in domain.indices:
|
||||
indexname = '%s-%s' % (domain.name, indexinfo[0])
|
||||
for indexcls in domain.indices:
|
||||
indexname = '%s-%s' % (domain.name, indexcls.name)
|
||||
if isinstance(indices_config, list):
|
||||
if indexname not in indices_config:
|
||||
continue
|
||||
@@ -285,13 +285,13 @@ class LaTeXTranslator(nodes.NodeVisitor):
|
||||
if indexname == 'py-modindex' and \
|
||||
not self.builder.config.latex_use_modindex:
|
||||
continue
|
||||
if not domain.has_index_entries(indexinfo[0],
|
||||
self.builder.docnames):
|
||||
content, collapsed = indexcls(domain).generate(
|
||||
self.builder.docnames)
|
||||
if not content:
|
||||
continue
|
||||
ret.append('\\renewcommand{\\indexname}{%s}\n' %
|
||||
indexinfo[1])
|
||||
generate(*domain.get_index(indexinfo[0],
|
||||
self.builder.docnames))
|
||||
indexcls.localname)
|
||||
generate(content, collapsed)
|
||||
|
||||
return ''.join(ret)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user