@@ -618,6 +618,7 @@ def get_transforms(self):
618618 return rst .Parser .get_transforms (self ) + [
619619 CreateNotebookSectionAnchors ,
620620 ReplaceAlertDivs ,
621+ CopyLinkedFiles ,
621622 ]
622623
623624 def parse (self , inputstring , document ):
@@ -1003,11 +1004,48 @@ def _set_empty_lines(node, options):
10031004 node .attributes [attr ] = value
10041005
10051006
1006- class ProcessLocalLinks (docutils .transforms .Transform ):
1007- """Process links to local files.
1007+ def _local_file_from_reference (node , document ):
1008+ """Get local file path from reference and split it into components."""
1009+ # NB: Anonymous hyperlinks must be already resolved at this point!
1010+ refuri = node .get ('refuri' )
1011+ if not refuri :
1012+ refname = node .get ('refname' )
1013+ if refname :
1014+ refid = document .nameids .get (refname )
1015+ else :
1016+ # NB: This can happen for anonymous hyperlinks
1017+ refid = node .get ('refid' )
1018+ target = document .ids .get (refid )
1019+ if not target :
1020+ # No corresponding target, Sphinx may warn later
1021+ return '' , '' , ''
1022+ refuri = target .get ('refuri' )
1023+ if not refuri :
1024+ # Target doesn't have URI
1025+ return '' , '' , ''
1026+ if '://' in refuri :
1027+ # Not a local link
1028+ return '' , '' , ''
1029+ elif refuri .startswith ('#' ) or refuri .startswith ('mailto:' ):
1030+ # Not a local link
1031+ return '' , '' , ''
1032+
1033+ # NB: We look for "fragment identifier" before unquoting
1034+ match = re .match (r'^([^#]+)(\.[^#]+)(#.+)$' , refuri )
1035+ if match :
1036+ base = unquote (match .group (1 ))
1037+ # NB: The suffix and "fragment identifier" are not unquoted
1038+ suffix = match .group (2 )
1039+ fragment = match .group (3 )
1040+ else :
1041+ base , suffix = os .path .splitext (refuri )
1042+ base = unquote (base )
1043+ fragment = ''
1044+ return base , suffix , fragment
1045+
10081046
1009- Marks local files to be copied to the HTML output directory and
1010- turns links to source files into ``:doc:``/``:ref:`` links.
1047+ class RewriteLocalLinks ( docutils . transforms . Transform ):
1048+ """Turn links to source files into ``:doc:``/``:ref:`` links.
10111049
10121050 Links to subsections are possible with ``...#Subsection-Title``.
10131051 These links use the labels created by CreateSectionLabels.
@@ -1027,69 +1065,28 @@ class ProcessLocalLinks(docutils.transforms.Transform):
10271065
10281066 default_priority = 500 # After AnonymousHyperlinks (440)
10291067
1030- _subsection_re = re .compile (r'^([^#]+)((\.[^#]+)#.+)$' )
1031-
10321068 def apply (self ):
10331069 env = self .document .settings .env
10341070 for node in self .document .traverse (docutils .nodes .reference ):
1035- # NB: Anonymous hyperlinks must be already resolved at this point!
1036- refuri = node .get ('refuri' )
1037- if not refuri :
1038- refname = node .get ('refname' )
1039- if refname :
1040- refid = self .document .nameids .get (refname )
1041- else :
1042- # NB: This can happen for anonymous hyperlinks
1043- refid = node .get ('refid' )
1044- target = self .document .ids .get (refid )
1045- if not target :
1046- continue # No corresponding target, Sphinx may warn later
1047- refuri = target .get ('refuri' )
1048- if not refuri :
1049- continue # Target doesn't have URI
1050-
1051- if '://' in refuri :
1052- continue # Not a local link
1053- elif refuri .startswith ('#' ) or refuri .startswith ('mailto:' ):
1054- continue # Nothing to be done
1055-
1056- # NB: We look for "fragment identifier" before unquoting
1057- fragment = self ._subsection_re .match (refuri )
1058- refuri = unquote (refuri )
1059- for suffix in env .config .source_suffix :
1060- if fragment :
1061- if fragment .group (3 ).lower () == suffix .lower ():
1062- target = unquote (fragment .group (1 ))
1063- # NB: The "fragment identifier" is not unquoted
1064- target_ext = fragment .group (2 )
1071+ base , suffix , fragment = _local_file_from_reference (node ,
1072+ self .document )
1073+ if not base :
1074+ continue
1075+
1076+ for s in env .config .source_suffix :
1077+ if suffix .lower () == s .lower ():
1078+ target = base
1079+ if fragment :
1080+ target_ext = suffix + fragment
10651081 reftype = 'ref'
10661082 refdomain = 'std'
1067- break
1068- else :
1069- if refuri .lower ().endswith (suffix .lower ()):
1070- target = refuri [:- len (suffix )]
1083+ else :
10711084 target_ext = ''
10721085 reftype = 'doc'
10731086 refdomain = None
1074- break
1087+ break
10751088 else :
1076- if fragment :
1077- refuri = unquote (fragment .group (1 )) + fragment .group (3 )
1078- file = os .path .normpath (
1079- os .path .join (os .path .dirname (env .docname ), refuri ))
1080- if not os .path .isfile (os .path .join (env .srcdir , file )):
1081- env .app .warn ('file not found: {!r}' .format (file ),
1082- env .doc2path (env .docname ))
1083- continue # Link is ignored
1084- elif file .startswith ('..' ):
1085- env .app .warn (
1086- 'link outside of source directory: {!r}' .format (file ),
1087- env .doc2path (env .docname ))
1088- continue # Link is ignored
1089- if not hasattr (env , 'nbsphinx_files' ):
1090- env .nbsphinx_files = {}
1091- env .nbsphinx_files .setdefault (env .docname , []).append (file )
1092- continue # We're done here
1089+ continue # Not a link to a potential Sphinx source file
10931090
10941091 target_docname = nbconvert .filters .posix_path (os .path .normpath (
10951092 os .path .join (os .path .dirname (env .docname ), target )))
@@ -1124,7 +1121,7 @@ def apply(self):
11241121class CreateSectionLabels (docutils .transforms .Transform ):
11251122 """Make labels for each document and each section thereof.
11261123
1127- These labels are referenced in ProcessLocalLinks but can also be
1124+ These labels are referenced in RewriteLocalLinks but can also be
11281125 used manually with ``:ref:``.
11291126
11301127 """
@@ -1235,6 +1232,35 @@ def apply(self):
12351232 content .append (sibling )
12361233
12371234
1235+ class CopyLinkedFiles (docutils .transforms .Transform ):
1236+ """Mark linked (local) files to be copied to the HTML output."""
1237+
1238+ default_priority = 600 # After RewriteLocalLinks
1239+
1240+ def apply (self ):
1241+ env = self .document .settings .env
1242+ for node in self .document .traverse (docutils .nodes .reference ):
1243+ base , suffix , fragment = _local_file_from_reference (node ,
1244+ self .document )
1245+ if not base :
1246+ continue # Not a local link
1247+ relpath = base + suffix + fragment
1248+ file = os .path .normpath (
1249+ os .path .join (os .path .dirname (env .docname ), relpath ))
1250+ if not os .path .isfile (os .path .join (env .srcdir , file )):
1251+ env .app .warn ('file not found: {!r}' .format (file ),
1252+ env .doc2path (env .docname ))
1253+ continue # Link is ignored
1254+ elif file .startswith ('..' ):
1255+ env .app .warn (
1256+ 'link outside of source directory: {!r}' .format (file ),
1257+ env .doc2path (env .docname ))
1258+ continue # Link is ignored
1259+ if not hasattr (env , 'nbsphinx_files' ):
1260+ env .nbsphinx_files = {}
1261+ env .nbsphinx_files .setdefault (env .docname , []).append (file )
1262+
1263+
12381264def builder_inited (app ):
12391265 # Add LaTeX definitions to preamble
12401266 latex_elements = app .builder .config .latex_elements
@@ -1453,7 +1479,7 @@ def setup(app):
14531479 app .connect ('env-purge-doc' , env_purge_doc )
14541480 app .add_transform (CreateSectionLabels )
14551481 app .add_transform (CreateDomainObjectLabels )
1456- app .add_transform (ProcessLocalLinks )
1482+ app .add_transform (RewriteLocalLinks )
14571483
14581484 # Make docutils' "code" directive (generated by markdown2rst/pandoc)
14591485 # behave like Sphinx's "code-block",
0 commit comments