Skip to content

Commit db81fe0

Browse files
committed
Copy locally linked files only for Jupyter notebooks
... while still re-writing local links to Sphinx source files in all Sphinx source files (not only notebooks). Fixes #174.
1 parent 89cbb76 commit db81fe0

File tree

2 files changed

+86
-73
lines changed

2 files changed

+86
-73
lines changed

doc/a-normal-rst-file.rst

Lines changed: 0 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -75,19 +75,6 @@ For example, see this subsection_.
7575
.. _subsection: subdir/a-notebook-in-a-subdir.ipynb#A-Sub-Section
7676
7777
78-
Links to Local Files (HTML only)
79-
--------------------------------
80-
81-
If you use any of the above-mentioned methods to link to a local file that
82-
*isn't* a Sphinx source file, it will be automatically copied to the HTML output
83-
directory, like it would if you `link from a notebook`__.
84-
85-
Alternatively, you can of course as always use Sphinx's download__ role.
86-
87-
__ markdown-cells.ipynb#Links-to-Local-Files-(HTML-only)
88-
__ http://www.sphinx-doc.org/en/stable/markup/inline.html#role-download
89-
90-
9178
Links to Notebooks, Ye Olde Way
9279
-------------------------------
9380

src/nbsphinx.py

Lines changed: 86 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -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):
11241121
class 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+
12381264
def 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

Comments
 (0)