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}