1"""The ``needs_string_links`` entries that turn codelinks' URL fields into links.
  2
  3They are registered once, at ``config-inited``: after the ``[codelinks]`` table is
  4loaded and the fields are registered, and before Sphinx-Needs validates and compiles
  5``needs_string_links`` (its ``compile_string_links`` listener, priority 551). Writing
  6them from inside the ``src-trace`` directive instead, at read time, loses them in every
  7``-j N`` worker and changes the configuration on every build.
  8
  9An entry cannot carry anything known only at read time -- a project's commit, or which
 10project a need came from -- so the remote URL field holds the fully formed URL itself,
 11and its entry is an identity link: the URL is the target, and the part after the
 12commit segment (``path#Lline``) is the name.
 13"""
 14
 15from __future__ import annotations
 16
 17from typing import Any
 18
 19from sphinx.application import Sphinx
 20from sphinx.config import Config as _SphinxConfig
 21
 22from sphinx_codelinks.config import (
 23    CodeLinksConfig,
 24    declared_link_types,
 25    is_back_link_name,
 26    need_id_refs_field_names,
 27)
 28
 29URL_LINK_REGEX = (
 30    r"^(?P<codelinks_url>[A-Za-z][A-Za-z0-9+.-]*://"
 31    r"(?:[^#?]*?/(?:[0-9a-f]{64}|[0-9a-f]{40})/(?P<codelinks_location>.+)|.+))$"
 32)
 33"""Captures a whole URL, and the part after a full commit hash segment when it has one.
 34
 35The name falls back to the whole URL when the URL has no commit segment -- a remote URL
 36pattern without ``{commit}``, or a project outside a git repository. A value without a
 37``scheme://`` prefix is not a URL and does not match, so it renders as text.
 38"""
 39
 40
 41def url_string_link(field: str) -> dict[str, Any]:
 42    """The identity string link for a field whose value is a fully formed URL."""
 43    return {
 44        "regex": URL_LINK_REGEX,
 45        "link_url": "{{codelinks_url}}",
 46        "link_name": "{{codelinks_location or codelinks_url}}",
 47        "options": [field],
 48    }
 49
 50
 51REF_LINK_REGEX = (
 52    r"^(?P<codelinks_value>"
 53    r"(?P<codelinks_url>[A-Za-z][A-Za-z0-9+.-]*://"
 54    r"(?:[^#?]*?/(?:[0-9a-f]{64}|[0-9a-f]{40})/(?P<codelinks_location>.+)|.+))"
 55    r"|(?P<codelinks_page>.+?)\.[^./]+#L(?P<codelinks_line>\d+)"
 56    r"|.+)$"
 57)
 58"""A reference field's entry: a remote URL (``scheme://``, as :data:`URL_LINK_REGEX`),
 59else a local value in the ``local-url`` field's shape, ``<file>.<ext>#L<line>`` relative
 60to the document, which links to the source page written beside the copied file; anything
 61else renders as text."""
 62
 63
 64def ref_url_string_link(field: str) -> dict[str, Any]:
 65    """The string link for a field of ``@need-ids:`` references.
 66
 67    A remote URL renders as :func:`url_string_link` does; a local value as the
 68    ``local-url`` field does (to the generated source page, named by the value itself).
 69    """
 70    return {
 71        "regex": REF_LINK_REGEX,
 72        "link_url": (
 73            "{% if codelinks_url %}{{codelinks_url}}"
 74            "{% elif codelinks_page %}{{codelinks_page}}.html#L-{{codelinks_line}}"
 75            "{% endif %}"
 76        ),
 77        "link_name": (
 78            "{% if codelinks_url %}{{codelinks_location or codelinks_url}}"
 79            "{% elif codelinks_page %}{{codelinks_value}}{% endif %}"
 80        ),
 81        "options": [field],
 82    }
 83
 84
 85def local_url_string_link(field: str) -> dict[str, Any]:
 86    """The string link for the local URL field.
 87
 88    Its value is the copied source file's path relative to the document
 89    (``../src/file.cpp#L3``), and the link points at the source page written beside
 90    that copy.
 91    """
 92    return {
 93        "regex": r"^(?P<value>.+?)\.[^\.]+#L(?P<lineno>\d+)",
 94        "link_url": "{{value}}.html#L-{{lineno}}",
 95        "link_name": "{{value}}#L{{lineno}}",
 96        "options": [field],
 97    }
 98
 99
[docs]100# @Register the URL fields' string links at config-inited, IMPL_URL_LINKS_1, impl, [FE_DEF]
101def register_string_links(_app: Sphinx, config: _SphinxConfig) -> None:
102    """Add the string links for the URL fields to ``needs_string_links``.
103
104    The value is rebound rather than mutated, as it may be the user's own conf.py object.
105    A value that is not a dict is left alone: Sphinx-Needs warns about it and ignores it.
106    """
107    codelinks_config = CodeLinksConfig.from_sphinx(config)
108    entries: dict[str, dict[str, Any]] = {}
109    if codelinks_config.set_local_url:
110        entries[codelinks_config.local_url_field] = local_url_string_link(
111            codelinks_config.local_url_field
112        )
113    projects = codelinks_config.projects
114    # a malformed ``projects`` is reported by ``check_sphinx_configuration``, later
115    if (
116        codelinks_config.set_remote_url
117        and isinstance(projects, dict)
118        and any(
119            isinstance(project, dict) and project.get("remote_url_pattern")
120            for project in projects.values()
121        )
122    ):
123        entries[codelinks_config.remote_url_field] = url_string_link(
124            codelinks_config.remote_url_field
125        )
126    # a field naming a link type or its back links is not registered (it is refused
127    # by ``check_sphinx_configuration``), so it gets no string link either
128    link_types = declared_link_types(config)
129    for field_name in need_id_refs_field_names(codelinks_config):
130        if field_name in link_types or is_back_link_name(field_name, link_types):
131            continue
132        entries[field_name] = ref_url_string_link(field_name)
133    existing = config.needs_string_links
134    if not entries or not isinstance(existing, dict):
135        return
136    config.needs_string_links = {**existing, **entries}