|
3 | 3 | import types
|
4 | 4 | from docutils import nodes
|
5 | 5 | import sphinx
|
| 6 | +from sphinx.ext.intersphinx import InventoryAdapter |
| 7 | +from sphinx.ext.intersphinx import missing_reference as sphinx_missing_reference |
6 | 8 | from sphinx.roles import XRefRole
|
7 | 9 | from sphinx.util.fileutil import copy_asset
|
8 | 10 | from sphinx.util import logging
|
@@ -139,6 +141,114 @@ def setup_sphinx_tabs(app, config):
|
139 | 141 | app.disconnect(listener_id)
|
140 | 142 |
|
141 | 143 |
|
| 144 | +def setup_intersphinx(app, config): |
| 145 | + """ |
| 146 | + Disconnect ``missing-reference`` from ``sphinx.ext.intershinx``. |
| 147 | +
|
| 148 | + As there is no way to hook into the ``missing_referece`` function to add |
| 149 | + some extra data to the docutils node returned by this function, we |
| 150 | + disconnect the original listener and add our custom one. |
| 151 | +
|
| 152 | + https://github.com/sphinx-doc/sphinx/blob/53c1dff/sphinx/ext/intersphinx.py |
| 153 | + """ |
| 154 | + if not app.config.hoverxref_intersphinx: |
| 155 | + # Do not disconnect original intersphinx missing-reference if the user |
| 156 | + # does not have hoverxref intersphinx enabled |
| 157 | + return |
| 158 | + |
| 159 | + if sphinx.version_info < (3, 0, 0): |
| 160 | + listeners = list(app.events.listeners.get('missing-reference').items()) |
| 161 | + else: |
| 162 | + listeners = [ |
| 163 | + (listener.id, listener.handler) |
| 164 | + for listener in app.events.listeners.get('missing-reference') |
| 165 | + ] |
| 166 | + for listener_id, function in listeners: |
| 167 | + module_name = inspect.getmodule(function).__name__ |
| 168 | + if module_name == 'sphinx.ext.intersphinx': |
| 169 | + app.disconnect(listener_id) |
| 170 | + |
| 171 | + |
| 172 | +def missing_reference(app, env, node, contnode): |
| 173 | + """ |
| 174 | + Override original ``missing_referece`` to add data into the node. |
| 175 | +
|
| 176 | + We call the original intersphinx extension and add hoverxref CSS classes |
| 177 | + plus the ``data-url`` to the node returned from it. |
| 178 | +
|
| 179 | + Sphinx intersphinx downloads all the ``objects.inv`` and load each of them |
| 180 | + into a "named inventory" and also updates the "main inventory". We check if |
| 181 | + reference is part of any of the "named invetories" the user defined in |
| 182 | + ``hoverxref_intersphinx`` and we add hoverxref to the node **only if** the |
| 183 | + reference is on those inventories. |
| 184 | +
|
| 185 | + See https://github.com/sphinx-doc/sphinx/blob/4d90277c/sphinx/ext/intersphinx.py#L244-L250 |
| 186 | + """ |
| 187 | + if not app.config.hoverxref_intersphinx: |
| 188 | + # Do nothing if the user doesn't have hoverxref intersphinx enabled |
| 189 | + return |
| 190 | + |
| 191 | + # We need to grab all the attributes before calling |
| 192 | + # ``sphinx_missing_reference`` because it modifies the node in-place |
| 193 | + domain = node.get('refdomain') # ``std`` if used on ``:ref:`` |
| 194 | + target = node['reftarget'] |
| 195 | + reftype = node['reftype'] |
| 196 | + |
| 197 | + # By default we skip adding hoverxref to the node to avoid possible |
| 198 | + # problems. We want to be sure we have to add hoverxref on it |
| 199 | + skip_node = True |
| 200 | + inventory_name_matched = None |
| 201 | + |
| 202 | + if domain == 'std': |
| 203 | + # Using ``:ref:`` manually, we could write intersphinx like: |
| 204 | + # :ref:`datetime <python:datetime.datetime>` |
| 205 | + # and the node will have these attribues: |
| 206 | + # refdomain: std |
| 207 | + # reftype: ref |
| 208 | + # reftarget: python:datetime.datetime |
| 209 | + # refexplicit: True |
| 210 | + if ':' in target: |
| 211 | + inventory_name, _ = target.split(':', 1) |
| 212 | + if inventory_name in app.config.hoverxref_intersphinx: |
| 213 | + skip_node = False |
| 214 | + inventory_name_matched = inventory_name |
| 215 | + else: |
| 216 | + # Using intersphinx via ``sphinx.ext.autodoc`` generates links for docstrings like: |
| 217 | + # :py:class:`float` |
| 218 | + # and the node will have these attribues: |
| 219 | + # refdomain: py |
| 220 | + # reftype: class |
| 221 | + # reftarget: float |
| 222 | + # refexplicit: False |
| 223 | + inventories = InventoryAdapter(env) |
| 224 | + |
| 225 | + for inventory_name in app.config.hoverxref_intersphinx: |
| 226 | + inventory = inventories.named_inventory.get(inventory_name, {}) |
| 227 | + if inventory.get(f'{domain}:{reftype}', {}).get(target) is not None: |
| 228 | + # The object **does** exist on the inventories defined by the |
| 229 | + # user: enable hoverxref on this node |
| 230 | + skip_node = False |
| 231 | + inventory_name_matched = inventory_name |
| 232 | + break |
| 233 | + |
| 234 | + newnode = sphinx_missing_reference(app, env, node, contnode) |
| 235 | + if newnode is not None and not skip_node: |
| 236 | + hoverxref_type = app.config.hoverxref_intersphinx_types.get(inventory_name_matched) |
| 237 | + if isinstance(hoverxref_type, dict): |
| 238 | + # Specific style for a particular reftype |
| 239 | + hoverxref_type = hoverxref_type.get(reftype) |
| 240 | + hoverxref_type = hoverxref_type or app.config.hoverxref_default_type |
| 241 | + |
| 242 | + classes = newnode.get('classes') |
| 243 | + classes.extend(['hoverxref', hoverxref_type]) |
| 244 | + newnode.replace_attr('classes', classes) |
| 245 | + newnode._hoverxref = { |
| 246 | + 'data-url': newnode.get('refuri'), |
| 247 | + } |
| 248 | + |
| 249 | + return newnode |
| 250 | + |
| 251 | + |
142 | 252 | def setup_translators(app):
|
143 | 253 | """
|
144 | 254 | Override translators respecting the one defined (if any).
|
@@ -255,6 +365,8 @@ def setup(app):
|
255 | 365 | app.add_config_value('hoverxref_ignore_refs', ['genindex', 'modindex', 'search'], 'env')
|
256 | 366 | app.add_config_value('hoverxref_role_types', {}, 'env')
|
257 | 367 | app.add_config_value('hoverxref_default_type', 'tooltip', 'env')
|
| 368 | + app.add_config_value('hoverxref_intersphinx', [], 'env') |
| 369 | + app.add_config_value('hoverxref_intersphinx_types', {}, 'env') |
258 | 370 | app.add_config_value('hoverxref_api_host', 'https://readthedocs.org', 'env')
|
259 | 371 |
|
260 | 372 | # Tooltipster settings
|
@@ -288,10 +400,13 @@ def setup(app):
|
288 | 400 |
|
289 | 401 | app.connect('config-inited', setup_domains)
|
290 | 402 | app.connect('config-inited', setup_sphinx_tabs)
|
| 403 | + app.connect('config-inited', setup_intersphinx) |
291 | 404 | app.connect('config-inited', is_hoverxref_configured)
|
292 | 405 | app.connect('config-inited', setup_theme)
|
293 | 406 | app.connect('build-finished', copy_asset_files)
|
294 | 407 |
|
| 408 | + app.connect('missing-reference', missing_reference) |
| 409 | + |
295 | 410 | for f in ASSETS_FILES:
|
296 | 411 | if f.endswith('.js') or f.endswith('.js_t'):
|
297 | 412 | app.add_js_file(f.replace('.js_t', '.js'))
|
|
0 commit comments