|
26 | 26 | import concurrent.futures
|
27 | 27 | import functools
|
28 | 28 | import posixpath
|
| 29 | +import re |
29 | 30 | import sys
|
30 | 31 | import time
|
31 | 32 | from os import path
|
32 |
| -from typing import IO, Any, Dict, List, Optional, Tuple |
| 33 | +from types import ModuleType |
| 34 | +from typing import IO, Any, Dict, List, Optional, Tuple, cast |
33 | 35 | from urllib.parse import urlsplit, urlunsplit
|
34 | 36 |
|
35 | 37 | from docutils import nodes
|
36 |
| -from docutils.nodes import Element, TextElement |
37 |
| -from docutils.utils import relative_path |
| 38 | +from docutils.nodes import Element, Node, TextElement, system_message |
| 39 | +from docutils.utils import Reporter, relative_path |
38 | 40 |
|
39 | 41 | import sphinx
|
40 | 42 | from sphinx.addnodes import pending_xref
|
|
43 | 45 | from sphinx.config import Config
|
44 | 46 | from sphinx.domains import Domain
|
45 | 47 | from sphinx.environment import BuildEnvironment
|
| 48 | +from sphinx.errors import ExtensionError |
46 | 49 | from sphinx.locale import _, __
|
| 50 | +from sphinx.transforms.post_transforms import ReferencesResolver |
47 | 51 | from sphinx.util import logging, requests
|
| 52 | +from sphinx.util.docutils import CustomReSTDispatcher, SphinxRole |
48 | 53 | from sphinx.util.inventory import InventoryFile
|
49 |
| -from sphinx.util.typing import Inventory, InventoryItem |
| 54 | +from sphinx.util.typing import Inventory, InventoryItem, RoleFunction |
50 | 55 |
|
51 | 56 | logger = logging.getLogger(__name__)
|
52 | 57 |
|
@@ -466,6 +471,144 @@ def missing_reference(app: Sphinx, env: BuildEnvironment, node: pending_xref,
|
466 | 471 | return resolve_reference_detect_inventory(env, node, contnode)
|
467 | 472 |
|
468 | 473 |
|
| 474 | +class IntersphinxDispatcher(CustomReSTDispatcher): |
| 475 | + """Custom dispatcher for external role. |
| 476 | +
|
| 477 | + This enables :external:***:/:external+***: roles on parsing reST document. |
| 478 | + """ |
| 479 | + |
| 480 | + def role(self, role_name: str, language_module: ModuleType, lineno: int, reporter: Reporter |
| 481 | + ) -> Tuple[RoleFunction, List[system_message]]: |
| 482 | + if len(role_name) > 9 and role_name.startswith(('external:', 'external+')): |
| 483 | + return IntersphinxRole(role_name), [] |
| 484 | + else: |
| 485 | + return super().role(role_name, language_module, lineno, reporter) |
| 486 | + |
| 487 | + |
| 488 | +class IntersphinxRole(SphinxRole): |
| 489 | + # group 1: just for the optionality of the inventory name |
| 490 | + # group 2: the inventory name (optional) |
| 491 | + # group 3: the domain:role or role part |
| 492 | + _re_inv_ref = re.compile(r"(\+([^:]+))?:(.*)") |
| 493 | + |
| 494 | + def __init__(self, orig_name: str) -> None: |
| 495 | + self.orig_name = orig_name |
| 496 | + |
| 497 | + def run(self) -> Tuple[List[Node], List[system_message]]: |
| 498 | + assert self.name == self.orig_name.lower() |
| 499 | + inventory, name_suffix = self.get_inventory_and_name_suffix(self.orig_name) |
| 500 | + if inventory and not inventory_exists(self.env, inventory): |
| 501 | + logger.warning(__('inventory for external cross-reference not found: %s'), |
| 502 | + inventory, location=(self.env.docname, self.lineno)) |
| 503 | + return [], [] |
| 504 | + |
| 505 | + role_name = self.get_role_name(name_suffix) |
| 506 | + if role_name is None: |
| 507 | + logger.warning(__('role for external cross-reference not found: %s'), name_suffix, |
| 508 | + location=(self.env.docname, self.lineno)) |
| 509 | + return [], [] |
| 510 | + |
| 511 | + result, messages = self.invoke_role(role_name) |
| 512 | + for node in result: |
| 513 | + if isinstance(node, pending_xref): |
| 514 | + node['intersphinx'] = True |
| 515 | + node['inventory'] = inventory |
| 516 | + |
| 517 | + return result, messages |
| 518 | + |
| 519 | + def get_inventory_and_name_suffix(self, name: str) -> Tuple[Optional[str], str]: |
| 520 | + assert name.startswith('external'), name |
| 521 | + assert name[8] in ':+', name |
| 522 | + # either we have an explicit inventory name, i.e, |
| 523 | + # :external+inv:role: or |
| 524 | + # :external+inv:domain:role: |
| 525 | + # or we look in all inventories, i.e., |
| 526 | + # :external:role: or |
| 527 | + # :external:domain:role: |
| 528 | + inv, suffix = IntersphinxRole._re_inv_ref.fullmatch(name, 8).group(2, 3) |
| 529 | + return inv, suffix |
| 530 | + |
| 531 | + def get_role_name(self, name: str) -> Optional[Tuple[str, str]]: |
| 532 | + names = name.split(':') |
| 533 | + if len(names) == 1: |
| 534 | + # role |
| 535 | + default_domain = self.env.temp_data.get('default_domain') |
| 536 | + domain = default_domain.name if default_domain else None |
| 537 | + role = names[0] |
| 538 | + elif len(names) == 2: |
| 539 | + # domain:role: |
| 540 | + domain = names[0] |
| 541 | + role = names[1] |
| 542 | + else: |
| 543 | + return None |
| 544 | + |
| 545 | + if domain and self.is_existent_role(domain, role): |
| 546 | + return (domain, role) |
| 547 | + elif self.is_existent_role('std', role): |
| 548 | + return ('std', role) |
| 549 | + else: |
| 550 | + return None |
| 551 | + |
| 552 | + def is_existent_role(self, domain_name: str, role_name: str) -> bool: |
| 553 | + try: |
| 554 | + domain = self.env.get_domain(domain_name) |
| 555 | + if role_name in domain.roles: |
| 556 | + return True |
| 557 | + else: |
| 558 | + return False |
| 559 | + except ExtensionError: |
| 560 | + return False |
| 561 | + |
| 562 | + def invoke_role(self, role: Tuple[str, str]) -> Tuple[List[Node], List[system_message]]: |
| 563 | + domain = self.env.get_domain(role[0]) |
| 564 | + if domain: |
| 565 | + role_func = domain.role(role[1]) |
| 566 | + |
| 567 | + return role_func(':'.join(role), self.rawtext, self.text, self.lineno, |
| 568 | + self.inliner, self.options, self.content) |
| 569 | + else: |
| 570 | + return [], [] |
| 571 | + |
| 572 | + |
| 573 | +class IntersphinxRoleResolver(ReferencesResolver): |
| 574 | + """pending_xref node resolver for intersphinx role. |
| 575 | +
|
| 576 | + This resolves pending_xref nodes generated by :intersphinx:***: role. |
| 577 | + """ |
| 578 | + |
| 579 | + default_priority = ReferencesResolver.default_priority - 1 |
| 580 | + |
| 581 | + def run(self, **kwargs: Any) -> None: |
| 582 | + for node in self.document.traverse(pending_xref): |
| 583 | + if 'intersphinx' not in node: |
| 584 | + continue |
| 585 | + contnode = cast(nodes.TextElement, node[0].deepcopy()) |
| 586 | + inv_name = node['inventory'] |
| 587 | + if inv_name is not None: |
| 588 | + assert inventory_exists(self.env, inv_name) |
| 589 | + newnode = resolve_reference_in_inventory(self.env, inv_name, node, contnode) |
| 590 | + else: |
| 591 | + newnode = resolve_reference_any_inventory(self.env, False, node, contnode) |
| 592 | + if newnode is None: |
| 593 | + typ = node['reftype'] |
| 594 | + msg = (__('external %s:%s reference target not found: %s') % |
| 595 | + (node['refdomain'], typ, node['reftarget'])) |
| 596 | + logger.warning(msg, location=node, type='ref', subtype=typ) |
| 597 | + node.replace_self(contnode) |
| 598 | + else: |
| 599 | + node.replace_self(newnode) |
| 600 | + |
| 601 | + |
| 602 | +def install_dispatcher(app: Sphinx, docname: str, source: List[str]) -> None: |
| 603 | + """Enable IntersphinxDispatcher. |
| 604 | +
|
| 605 | + .. note:: The installed dispatcher will uninstalled on disabling sphinx_domain |
| 606 | + automatically. |
| 607 | + """ |
| 608 | + dispatcher = IntersphinxDispatcher() |
| 609 | + dispatcher.enable() |
| 610 | + |
| 611 | + |
469 | 612 | def normalize_intersphinx_mapping(app: Sphinx, config: Config) -> None:
|
470 | 613 | for key, value in config.intersphinx_mapping.copy().items():
|
471 | 614 | try:
|
@@ -497,7 +640,9 @@ def setup(app: Sphinx) -> Dict[str, Any]:
|
497 | 640 | app.add_config_value('intersphinx_disabled_reftypes', [], True)
|
498 | 641 | app.connect('config-inited', normalize_intersphinx_mapping, priority=800)
|
499 | 642 | app.connect('builder-inited', load_mappings)
|
| 643 | + app.connect('source-read', install_dispatcher) |
500 | 644 | app.connect('missing-reference', missing_reference)
|
| 645 | + app.add_post_transform(IntersphinxRoleResolver) |
501 | 646 | return {
|
502 | 647 | 'version': sphinx.__display_version__,
|
503 | 648 | 'env_version': 1,
|
|
0 commit comments