@@ -136,6 +136,8 @@ def setup_render(
136136 self ._level_to_elem : dict [int , nodes .document | nodes .section ] = {
137137 0 : self .document
138138 }
139+ # mapping of section slug to section node
140+ self ._slug_to_section : dict [str , nodes .section ] = {}
139141
140142 @property
141143 def sphinx_env (self ) -> BuildEnvironment | None :
@@ -236,6 +238,37 @@ def _render_initialise(self) -> None:
236238 def _render_finalise (self ) -> None :
237239 """Finalise the render of the document."""
238240
241+ # attempt to replace id_link references with internal links
242+ for refnode in findall (self .document )(nodes .reference ):
243+ if not refnode .get ("id_link" ):
244+ continue
245+ target = refnode ["refuri" ][1 :]
246+ if target in self ._slug_to_section :
247+ section_node = self ._slug_to_section [target ]
248+ refnode ["refid" ] = section_node ["ids" ][0 ]
249+
250+ if not refnode .children :
251+ implicit_text = clean_astext (section_node [0 ])
252+ refnode += nodes .inline (
253+ implicit_text , implicit_text , classes = ["std" , "std-ref" ]
254+ )
255+ else :
256+ self .create_warning (
257+ f"local id not found: { refnode ['refuri' ]!r} " ,
258+ MystWarnings .XREF_MISSING ,
259+ line = refnode .line ,
260+ append_to = refnode ,
261+ )
262+ refnode ["refid" ] = target
263+ del refnode ["refuri" ]
264+
265+ if self ._slug_to_section and self .sphinx_env :
266+ # save for later reference resolution
267+ self .sphinx_env .metadata [self .sphinx_env .docname ]["myst_slugs" ] = {
268+ slug : (snode ["ids" ][0 ], clean_astext (snode [0 ]))
269+ for slug , snode in self ._slug_to_section .items ()
270+ }
271+
239272 # log warnings for duplicate reference definitions
240273 # "duplicate_refs": [{"href": "ijk", "label": "B", "map": [4, 5], "title": ""}],
241274 for dup_ref in self .md_env .get ("duplicate_refs" , []):
@@ -713,11 +746,29 @@ def render_heading(self, token: SyntaxTreeNode) -> None:
713746 with self .current_node_context (title_node ):
714747 self .render_children (token )
715748
716- # create a target reference for the section, based on the heading text
749+ # create a target reference for the section, based on the heading text.
750+ # Note, this is an implicit target, meaning that it is not prioritised,
751+ # and is not stored by sphinx for ref resolution
717752 name = nodes .fully_normalize_name (title_node .astext ())
718753 new_section ["names" ].append (name )
719754 self .document .note_implicit_target (new_section , new_section )
720755
756+ # add possible reference slug, this may be different to the standard name above,
757+ # and does not have to be normalised, so we treat it separately
758+ if "id" in token .attrs :
759+ slug = str (token .attrs ["id" ])
760+ new_section ["slug" ] = slug
761+ if slug in self ._slug_to_section :
762+ other_node = self ._slug_to_section [slug ]
763+ self .create_warning (
764+ f"duplicate heading slug { slug !r} , other at line { other_node .line } " ,
765+ MystWarnings .ANCHOR_DUPE ,
766+ line = new_section .line ,
767+ )
768+ else :
769+ # we store this for later processing on finalise
770+ self ._slug_to_section [slug ] = new_section
771+
721772 # set the section as the current node for subsequent rendering
722773 self .current_node = new_section
723774
@@ -736,19 +787,19 @@ def render_link(self, token: SyntaxTreeNode) -> None:
736787 or self .md_config .gfm_only
737788 or self .md_config .all_links_external
738789 ):
739- if token .info == "auto" : # handles both autolink and linkify
740- return self .render_autolink (token )
741- else :
742- return self .render_external_url (token )
790+ return self .render_external_url (token )
743791
744792 href = cast (str , token .attrGet ("href" ) or "" )
745793
794+ if href .startswith ("#" ):
795+ return self .render_id_link (token )
796+
746797 # TODO ideally whether inv_link is enabled could be precomputed
747798 if "inv_link" in self .md_config .enable_extensions and href .startswith ("inv:" ):
748799 return self .create_inventory_link (token )
749800
750801 if token .info == "auto" : # handles both autolink and linkify
751- return self .render_autolink (token )
802+ return self .render_external_url (token )
752803
753804 # Check for external URL
754805 url_scheme = urlparse (href ).scheme
@@ -761,20 +812,27 @@ def render_link(self, token: SyntaxTreeNode) -> None:
761812 return self .render_internal_link (token )
762813
763814 def render_external_url (self , token : SyntaxTreeNode ) -> None :
764- """Render link token `[text](link "title")`,
765- where the link has been identified as an external URL::
766-
767- <reference refuri="link" title="title">
768- text
769-
770- `text` can contain nested syntax, e.g. `[**bold**](url "title")`.
815+ """Render link token (including autolink and linkify),
816+ where the link has been identified as an external URL.
771817 """
772818 ref_node = nodes .reference ()
773819 self .add_line_and_source_path (ref_node , token )
774820 self .copy_attributes (
775821 token , ref_node , ("class" , "id" , "reftitle" ), aliases = {"title" : "reftitle" }
776822 )
777- ref_node ["refuri" ] = cast (str , token .attrGet ("href" ) or "" )
823+ ref_node ["refuri" ] = escapeHtml (token .attrGet ("href" ) or "" ) # type: ignore[arg-type]
824+ with self .current_node_context (ref_node , append = True ):
825+ self .render_children (token )
826+
827+ def render_id_link (self , token : SyntaxTreeNode ) -> None :
828+ """Render link token like `[text](#id)`, to a local target."""
829+ ref_node = nodes .reference ()
830+ self .add_line_and_source_path (ref_node , token )
831+ ref_node ["id_link" ] = True
832+ ref_node ["refuri" ] = token .attrGet ("href" ) or ""
833+ self .copy_attributes (
834+ token , ref_node , ("class" , "id" , "reftitle" ), aliases = {"title" : "reftitle" }
835+ )
778836 with self .current_node_context (ref_node , append = True ):
779837 self .render_children (token )
780838
@@ -799,17 +857,6 @@ def render_internal_link(self, token: SyntaxTreeNode) -> None:
799857 with self .current_node_context (ref_node , append = True ):
800858 self .render_children (token )
801859
802- def render_autolink (self , token : SyntaxTreeNode ) -> None :
803- refuri = escapeHtml (token .attrGet ("href" ) or "" ) # type: ignore[arg-type]
804- ref_node = nodes .reference ()
805- self .copy_attributes (
806- token , ref_node , ("class" , "id" , "reftitle" ), aliases = {"title" : "reftitle" }
807- )
808- ref_node ["refuri" ] = refuri
809- self .add_line_and_source_path (ref_node , token )
810- with self .current_node_context (ref_node , append = True ):
811- self .render_children (token )
812-
813860 def create_inventory_link (self , token : SyntaxTreeNode ) -> None :
814861 r"""Create a link to an inventory object.
815862
@@ -1641,3 +1688,15 @@ def html_meta_to_nodes(
16411688 output .append (pending )
16421689
16431690 return output
1691+
1692+
1693+ def clean_astext (node : nodes .Element ) -> str :
1694+ """Like node.astext(), but ignore images.
1695+ Copied from sphinx.
1696+ """
1697+ node = node .deepcopy ()
1698+ for img in findall (node )(nodes .image ):
1699+ img ["alt" ] = ""
1700+ for raw in list (findall (node )(nodes .raw )):
1701+ raw .parent .remove (raw )
1702+ return node .astext ()
0 commit comments