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 CodeLinksConfig, need_id_refs_fields
 23
 24URL_LINK_REGEX = (
 25    r"^(?P<codelinks_url>[A-Za-z][A-Za-z0-9+.-]*://"
 26    r"(?:[^#?]*?/(?:[0-9a-f]{64}|[0-9a-f]{40})/(?P<codelinks_location>.+)|.+))$"
 27)
 28"""Captures a whole URL, and the part after a full commit hash segment when it has one.
 29
 30The name falls back to the whole URL when the URL has no commit segment -- a remote URL
 31pattern without ``{commit}``, or a project outside a git repository. A value without a
 32``scheme://`` prefix is not a URL and does not match, so it renders as text.
 33"""
 34
 35
 36def url_string_link(field: str) -> dict[str, Any]:
 37    """The identity string link for a field whose value is a fully formed URL."""
 38    return {
 39        "regex": URL_LINK_REGEX,
 40        "link_url": "{{codelinks_url}}",
 41        "link_name": "{{codelinks_location or codelinks_url}}",
 42        "options": [field],
 43    }
 44
 45
 46REF_LINK_REGEX = (
 47    r"^(?P<codelinks_value>"
 48    r"(?P<codelinks_url>[A-Za-z][A-Za-z0-9+.-]*://"
 49    r"(?:[^#?]*?/(?:[0-9a-f]{64}|[0-9a-f]{40})/(?P<codelinks_location>.+)|.+))"
 50    r"|(?P<codelinks_page>.+?)\.[^./]+#L(?P<codelinks_line>\d+)"
 51    r"|.+)$"
 52)
 53"""A reference field's entry: a remote URL (``scheme://``, as :data:`URL_LINK_REGEX`),
 54else a local value in the ``local-url`` field's shape, ``<file>.<ext>#L<line>`` relative
 55to the document, which links to the source page written beside the copied file; anything
 56else renders as text."""
 57
 58
 59def ref_url_string_link(field: str) -> dict[str, Any]:
 60    """The string link for a field of ``@need-ids:`` references.
 61
 62    A remote URL renders as :func:`url_string_link` does; a local value as the
 63    ``local-url`` field does (to the generated source page, named by the value itself).
 64    """
 65    return {
 66        "regex": REF_LINK_REGEX,
 67        "link_url": (
 68            "{% if codelinks_url %}{{codelinks_url}}"
 69            "{% elif codelinks_page %}{{codelinks_page}}.html#L-{{codelinks_line}}"
 70            "{% endif %}"
 71        ),
 72        "link_name": (
 73            "{% if codelinks_url %}{{codelinks_location or codelinks_url}}"
 74            "{% elif codelinks_page %}{{codelinks_value}}{% endif %}"
 75        ),
 76        "options": [field],
 77    }
 78
 79
 80def local_url_string_link(field: str) -> dict[str, Any]:
 81    """The string link for the local URL field.
 82
 83    Its value is the copied source file's path relative to the document
 84    (``../src/file.cpp#L3``), and the link points at the source page written beside
 85    that copy.
 86    """
 87    return {
 88        "regex": r"^(?P<value>.+?)\.[^\.]+#L(?P<lineno>\d+)",
 89        "link_url": "{{value}}.html#L-{{lineno}}",
 90        "link_name": "{{value}}#L{{lineno}}",
 91        "options": [field],
 92    }
 93
 94
[docs] 95# @Register the URL fields' string links at config-inited, IMPL_URL_LINKS_1, impl, [FE_DEF]
 96def register_string_links(_app: Sphinx, config: _SphinxConfig) -> None:
 97    """Add the string links for the URL fields to ``needs_string_links``.
 98
 99    The value is rebound rather than mutated, as it may be the user's own conf.py object.
100    A value that is not a dict is left alone: Sphinx-Needs warns about it and ignores it.
101    """
102    codelinks_config = CodeLinksConfig.from_sphinx(config)
103    entries: dict[str, dict[str, Any]] = {}
104    if codelinks_config.set_local_url:
105        entries[codelinks_config.local_url_field] = local_url_string_link(
106            codelinks_config.local_url_field
107        )
108    projects = codelinks_config.projects
109    # a malformed ``projects`` is reported by ``check_sphinx_configuration``, later
110    if (
111        codelinks_config.set_remote_url
112        and isinstance(projects, dict)
113        and any(
114            isinstance(project, dict) and project.get("remote_url_pattern")
115            for project in projects.values()
116        )
117    ):
118        entries[codelinks_config.remote_url_field] = url_string_link(
119            codelinks_config.remote_url_field
120        )
121    for field_name in set(need_id_refs_fields(codelinks_config).values()):
122        entries[field_name] = ref_url_string_link(field_name)
123    existing = config.needs_string_links
124    if not entries or not isinstance(existing, dict):
125        return
126    config.needs_string_links = {**existing, **entries}