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}