@@ -136,6 +136,8 @@ def setup_render(
136
136
self ._level_to_elem : dict [int , nodes .document | nodes .section ] = {
137
137
0 : self .document
138
138
}
139
+ # mapping of section slug to section node
140
+ self ._slug_to_section : dict [str , nodes .section ] = {}
139
141
140
142
@property
141
143
def sphinx_env (self ) -> BuildEnvironment | None :
@@ -236,6 +238,37 @@ def _render_initialise(self) -> None:
236
238
def _render_finalise (self ) -> None :
237
239
"""Finalise the render of the document."""
238
240
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
+
239
272
# log warnings for duplicate reference definitions
240
273
# "duplicate_refs": [{"href": "ijk", "label": "B", "map": [4, 5], "title": ""}],
241
274
for dup_ref in self .md_env .get ("duplicate_refs" , []):
@@ -713,11 +746,29 @@ def render_heading(self, token: SyntaxTreeNode) -> None:
713
746
with self .current_node_context (title_node ):
714
747
self .render_children (token )
715
748
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
717
752
name = nodes .fully_normalize_name (title_node .astext ())
718
753
new_section ["names" ].append (name )
719
754
self .document .note_implicit_target (new_section , new_section )
720
755
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
+
721
772
# set the section as the current node for subsequent rendering
722
773
self .current_node = new_section
723
774
@@ -736,19 +787,19 @@ def render_link(self, token: SyntaxTreeNode) -> None:
736
787
or self .md_config .gfm_only
737
788
or self .md_config .all_links_external
738
789
):
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 )
743
791
744
792
href = cast (str , token .attrGet ("href" ) or "" )
745
793
794
+ if href .startswith ("#" ):
795
+ return self .render_id_link (token )
796
+
746
797
# TODO ideally whether inv_link is enabled could be precomputed
747
798
if "inv_link" in self .md_config .enable_extensions and href .startswith ("inv:" ):
748
799
return self .create_inventory_link (token )
749
800
750
801
if token .info == "auto" : # handles both autolink and linkify
751
- return self .render_autolink (token )
802
+ return self .render_external_url (token )
752
803
753
804
# Check for external URL
754
805
url_scheme = urlparse (href ).scheme
@@ -761,20 +812,27 @@ def render_link(self, token: SyntaxTreeNode) -> None:
761
812
return self .render_internal_link (token )
762
813
763
814
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.
771
817
"""
772
818
ref_node = nodes .reference ()
773
819
self .add_line_and_source_path (ref_node , token )
774
820
self .copy_attributes (
775
821
token , ref_node , ("class" , "id" , "reftitle" ), aliases = {"title" : "reftitle" }
776
822
)
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
+ )
778
836
with self .current_node_context (ref_node , append = True ):
779
837
self .render_children (token )
780
838
@@ -799,17 +857,6 @@ def render_internal_link(self, token: SyntaxTreeNode) -> None:
799
857
with self .current_node_context (ref_node , append = True ):
800
858
self .render_children (token )
801
859
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
-
813
860
def create_inventory_link (self , token : SyntaxTreeNode ) -> None :
814
861
r"""Create a link to an inventory object.
815
862
@@ -1641,3 +1688,15 @@ def html_meta_to_nodes(
1641
1688
output .append (pending )
1642
1689
1643
1690
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 node .findall (nodes .image ):
1699
+ img ["alt" ] = ""
1700
+ for raw in list (node .findall (nodes .raw )):
1701
+ raw .parent .remove (raw )
1702
+ return node .astext ()
0 commit comments