@@ -53,6 +53,15 @@ class ScenarioAppendixPlaceholder(nodes.General, nodes.Element):
5353 """Replaced by the full appendix content during the resolve phase."""
5454
5555
56+ class ScenarioAppendixRef (nodes .General , nodes .Inline , nodes .Element ):
57+ """Deferred cross-reference to a scenario appendix entry.
58+
59+ Stores ``label`` and ``reftitle`` as attributes. Resolved to a proper
60+ ``nodes.reference`` with ``refdocname`` set during ``doctree-resolved``,
61+ once the appendix document name is known from the environment.
62+ """
63+
64+
5665# ---------------------------------------------------------------------------
5766# Helper functions
5867# ---------------------------------------------------------------------------
@@ -276,13 +285,18 @@ def _render_pdf(
276285
277286 # ----------------------------------------------------------
278287 # Return a short paragraph pointing to the appendix.
279- # nodes.reference(internal=True, refid=...) produces a
280- # \hyperref in LaTeX, which works within one compiled PDF.
288+ # Use a deferred ScenarioAppendixRef node; it is resolved to a
289+ # nodes.reference with refdocname set in doctree-resolved, once
290+ # the appendix document name is known. Sphinx's LaTeX writer
291+ # requires refdocname to construct the \hyperref[docname:id]
292+ # label correctly — omitting it silently drops the hyperlink.
281293 # ----------------------------------------------------------
294+ ref_node = ScenarioAppendixRef ()
295+ ref_node ["label" ] = label
296+ ref_node ["reftitle" ] = title
282297 para = nodes .paragraph ()
283298 para += nodes .emphasis (text = "Scenarios: see " )
284- ref = nodes .reference ("" , title , internal = True , refid = label )
285- para += ref
299+ para += ref_node
286300 para += nodes .emphasis (text = " in the appendix." )
287301 return [para ]
288302
@@ -341,6 +355,9 @@ def run(self) -> List[nodes.Node]:
341355 note += para
342356 return [note ]
343357
358+ # Record which document hosts the appendix so that ScenarioAppendixRef
359+ # nodes in other documents can be resolved with the correct refdocname.
360+ env .scenario_appendix_docname = env .docname
344361 node = ScenarioAppendixPlaceholder ()
345362 return [node ]
346363
@@ -386,6 +403,29 @@ def _build_appendix_nodes(entries: Dict) -> List[nodes.Node]:
386403 return result
387404
388405
406+ def resolve_scenario_appendix_refs (
407+ app , doctree : nodes .document , _fromdocname : str
408+ ) -> None :
409+ """Replace ScenarioAppendixRef nodes with resolved cross-references.
410+
411+ Called for every document during doctree-resolved. By that point all
412+ source files have been read, so ``env.scenario_appendix_docname`` is set.
413+ Sphinx's LaTeX writer needs ``refdocname`` on internal references to build
414+ the ``docname:id`` label key used for ``\\ hyperref`` targets.
415+ """
416+ appendix_docname = getattr (app .env , "scenario_appendix_docname" , None )
417+ for ref_node in doctree .traverse (ScenarioAppendixRef ):
418+ label = ref_node ["label" ]
419+ title = ref_node ["reftitle" ]
420+ if appendix_docname :
421+ ref = nodes .reference (
422+ "" , title , internal = True , refid = label , refdocname = appendix_docname
423+ )
424+ else :
425+ ref = nodes .inline ("" , title )
426+ ref_node .replace_self (ref )
427+
428+
389429def process_scenario_appendix (app , doctree : nodes .document , _fromdocname : str ) -> None :
390430 """Replace ScenarioAppendixPlaceholder nodes with generated content."""
391431 placeholders = list (doctree .traverse (ScenarioAppendixPlaceholder ))
@@ -414,6 +454,10 @@ def purge_scenario_appendix(_app, env, docname: str) -> None:
414454
415455def merge_scenario_appendix (_app , env , _docnames , other ) -> None :
416456 """Merge appendix entries from a parallel read worker."""
457+ if hasattr (other , "scenario_appendix_docname" ) and not hasattr (
458+ env , "scenario_appendix_docname"
459+ ):
460+ env .scenario_appendix_docname = other .scenario_appendix_docname
417461 if not hasattr (env , "scenario_appendix_entries" ):
418462 env .scenario_appendix_entries = {}
419463 for key , entry in getattr (other , "scenario_appendix_entries" , {}).items ():
@@ -438,6 +482,8 @@ def setup(app):
438482 app .add_directive ("scenario-include" , ScenarioIncludeDirective )
439483 app .add_directive ("scenario-appendix" , ScenarioAppendixDirective )
440484 app .add_node (ScenarioAppendixPlaceholder )
485+ app .add_node (ScenarioAppendixRef )
486+ app .connect ("doctree-resolved" , resolve_scenario_appendix_refs )
441487 app .connect ("doctree-resolved" , process_scenario_appendix )
442488 app .connect ("env-purge-doc" , purge_scenario_appendix )
443489 app .connect ("env-merge-info" , merge_scenario_appendix )
0 commit comments