Skip to content

Commit 7e79165

Browse files
committed
Rewrite local links for RST (and other source) files, too
1 parent 3c3d21a commit 7e79165

File tree

3 files changed

+176
-47
lines changed

3 files changed

+176
-47
lines changed

doc/a-normal-rst-file.rst

Lines changed: 112 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -5,21 +5,122 @@ This is a normal RST file.
55

66
.. note:: Those still work!
77

8-
Links to Notebooks
9-
------------------
8+
Links to Notebooks (and Other Sphinx Source Files)
9+
--------------------------------------------------
1010

11-
Links to notebooks can be easily created:
12-
:ref:`/subdir/a-notebook-in-a-subdir.ipynb`
13-
(the notebook title is used as link text).
14-
You can also use
15-
:ref:`an alternative text </subdir/a-notebook-in-a-subdir.ipynb>`.
11+
Links to Sphinx source files can be created like normal `Sphinx hyperlinks`_,
12+
just using a relative path to the local file: link_.
1613

17-
The above links were created with:
14+
.. _Sphinx hyperlinks: http://www.sphinx-doc.org/en/stable/rest.html
15+
#external-links
16+
.. _link: subdir/a-notebook-in-a-subdir.ipynb
17+
18+
.. code-block:: rst
19+
20+
using a relative path to the local file: link_.
21+
22+
.. _link: subdir/a-notebook-in-a-subdir.ipynb
23+
24+
If the link text has a space (or some other strange character) in it, you have
25+
to surround it with backticks: `a notebook link`_.
26+
27+
.. _a notebook link: subdir/a-notebook-in-a-subdir.ipynb
28+
29+
.. code-block:: rst
30+
31+
surround it with backticks: `a notebook link`_.
32+
33+
.. _a notebook link: subdir/a-notebook-in-a-subdir.ipynb
34+
35+
You can also use an `anonymous hyperlink target`_, like this: link__.
36+
If you have multiple of those, their order matters!
37+
38+
.. _anonymous hyperlink target: http://docutils.sourceforge.net/docs/ref/rst/
39+
restructuredtext.html#anonymous-hyperlinks
40+
41+
__ subdir/a-notebook-in-a-subdir.ipynb
42+
43+
.. code-block:: rst
44+
45+
like this: link__.
46+
47+
__ subdir/a-notebook-in-a-subdir.ipynb
48+
49+
Finally, you can use `Embedded URIs`_, like this
50+
`link <subdir/a-notebook-in-a-subdir.ipynb>`_.
51+
52+
.. _Embedded URIs: http://docutils.sourceforge.net/docs/ref/rst/
53+
restructuredtext.html#embedded-uris-and-aliases
54+
55+
.. code-block:: rst
56+
57+
like this `link <subdir/a-notebook-in-a-subdir.ipynb>`_.
58+
59+
.. note::
60+
61+
These links should also work on Github and in other rendered
62+
reStructuredText pages.
63+
64+
Links to subsections are also possible by adding a hash sign (``#``) and the
65+
section title to any of the above-mentioned link variants.
66+
You have to replace spaces in the section titles by hyphens.
67+
For example, see this subsection_.
68+
69+
.. _subsection: subdir/a-notebook-in-a-subdir.ipynb#A-Sub-Section
70+
71+
.. code-block:: rst
72+
73+
For example, see this subsection_.
74+
75+
.. _subsection: subdir/a-notebook-in-a-subdir.ipynb#A-Sub-Section
76+
77+
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+
91+
Links to Notebooks, Ye Olde Way
92+
-------------------------------
93+
94+
In addition to the way shown above, you can also create links to notebooks (and
95+
other Sphinx source files) with
96+
`:ref: <http://www.sphinx-doc.org/en/stable/markup/inline.html#role-ref>`_.
97+
This has some disadvantages:
98+
99+
* It is arguably a bit more clunky.
100+
* Because ``:ref:`` is a Sphinx feature, the links don't work on Github and
101+
other rendered reStructuredText pages that use plain old ``docutils``.
102+
103+
It also has one important advantage:
104+
105+
* The link text can automatically be taken from the actual section title.
106+
107+
A link with automatic title looks like this:
108+
:ref:`/subdir/a-notebook-in-a-subdir.ipynb`.
18109

19110
.. code-block:: rst
20111
21112
:ref:`/subdir/a-notebook-in-a-subdir.ipynb`
22-
:ref:`an alternative text </subdir/a-notebook-in-a-subdir.ipynb>`
113+
114+
But you can also provide
115+
:ref:`your own link title </subdir/a-notebook-in-a-subdir.ipynb>`.
116+
117+
.. code-block:: rst
118+
119+
:ref:`your own link title </subdir/a-notebook-in-a-subdir.ipynb>`
120+
121+
However, if you want to use your own title, you are probably better off using
122+
the method described above in
123+
`Links to Notebooks (and Other Sphinx Source Files)`_.
23124

24125
Links to subsections are also possible, e.g.
25126
:ref:`/subdir/a-notebook-in-a-subdir.ipynb#A-Sub-Section`
@@ -35,9 +136,9 @@ These links were created with:
35136
36137
.. note::
37138

139+
* The paths have to be relative to the top source directory and they have to
140+
start with a slash (``/``).
38141
* Spaces in the section title have to be replaced by hyphens!
39-
* Notebook paths have to be relative to the top source directory and they
40-
have to start with a slash (``/``).
41142

42143
Sphinx Directives for Info/Warning Boxes
43144
----------------------------------------

doc/orphan.ipynb

Lines changed: 2 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -23,16 +23,10 @@
2323
" [some link text](notebookname.ipynb)\n",
2424
" ```\n",
2525
"\n",
26-
"* ... from a [reST page](a-normal-rst-file.rst#Links-to-Notebooks) using\n",
26+
"* ... from a [reST page](a-normal-rst-file.rst#links-to-notebooks-and-other-sphinx-source-files) using\n",
2727
"\n",
2828
" ```rst\n",
29-
" :ref:`/notebookname.ipynb`\n",
30-
" ```\n",
31-
"\n",
32-
" or\n",
33-
"\n",
34-
" ```rst\n",
35-
" :ref:`alternative link text </notebookname.ipynb>`\n",
29+
" `some link text <notebookname.ipynb>`_\n",
3630
" ```\n",
3731
"\n",
3832
"Sphinx raises a warning in case of orphaned documents:\n",

src/nbsphinx.py

Lines changed: 62 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -616,7 +616,9 @@ class NotebookParser(rst.Parser):
616616
def get_transforms(self):
617617
"""List of transforms for documents parsed by this parser."""
618618
return rst.Parser.get_transforms(self) + [
619-
ProcessLocalLinks, ReplaceAlertDivs]
619+
CreateNotebookSectionAnchors,
620+
ReplaceAlertDivs,
621+
]
620622

621623
def parse(self, inputstring, document):
622624
"""Parse *inputstring*, write results to *document*.
@@ -1005,12 +1007,12 @@ class ProcessLocalLinks(docutils.transforms.Transform):
10051007
"""Process links to local files.
10061008
10071009
Marks local files to be copied to the HTML output directory and
1008-
turns links to local notebooks into ``:doc:``/``:ref:`` links.
1010+
turns links to source files into ``:doc:``/``:ref:`` links.
10091011
10101012
Links to subsections are possible with ``...#Subsection-Title``.
10111013
These links use the labels created by CreateSectionLabels.
10121014
1013-
Links to subsections use ``:ref:``, links to whole notebooks use
1015+
Links to subsections use ``:ref:``, links to whole source files use
10141016
``:doc:``. Latter can be useful if you have an ``index.rst`` but
10151017
also want a distinct ``index.ipynb`` for use with Jupyter.
10161018
In this case you can use such a link in a notebook::
@@ -1023,24 +1025,37 @@ class ProcessLocalLinks(docutils.transforms.Transform):
10231025
10241026
"""
10251027

1026-
default_priority = 400 # Should probably be adjusted?
1028+
default_priority = 500 # After AnonymousHyperlinks (440)
10271029

10281030
_subsection_re = re.compile(r'^([^#]+)((\.[^#]+)#.+)$')
10291031

10301032
def apply(self):
10311033
env = self.document.settings.env
10321034
for node in self.document.traverse(docutils.nodes.reference):
1033-
uri = node.get('refuri', '')
1034-
if not uri:
1035-
continue # No URI (e.g. named reference)
1036-
elif '://' in uri:
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:
10371052
continue # Not a local link
1038-
elif uri.startswith('#') or uri.startswith('mailto:'):
1053+
elif refuri.startswith('#') or refuri.startswith('mailto:'):
10391054
continue # Nothing to be done
10401055

10411056
# NB: We look for "fragment identifier" before unquoting
1042-
fragment = self._subsection_re.match(uri)
1043-
uri = unquote(uri)
1057+
fragment = self._subsection_re.match(refuri)
1058+
refuri = unquote(refuri)
10441059
for suffix in env.config.source_suffix:
10451060
if fragment:
10461061
if fragment.group(3).lower() == suffix.lower():
@@ -1051,17 +1066,17 @@ def apply(self):
10511066
refdomain = 'std'
10521067
break
10531068
else:
1054-
if uri.lower().endswith(suffix.lower()):
1055-
target = uri[:-len(suffix)]
1069+
if refuri.lower().endswith(suffix.lower()):
1070+
target = refuri[:-len(suffix)]
10561071
target_ext = ''
10571072
reftype = 'doc'
10581073
refdomain = None
10591074
break
10601075
else:
10611076
if fragment:
1062-
uri = unquote(fragment.group(1)) + fragment.group(3)
1077+
refuri = unquote(fragment.group(1)) + fragment.group(3)
10631078
file = os.path.normpath(
1064-
os.path.join(os.path.dirname(env.docname), uri))
1079+
os.path.join(os.path.dirname(env.docname), refuri))
10651080
if not os.path.isfile(os.path.join(env.srcdir, file)):
10661081
env.app.warn('file not found: {!r}'.format(file),
10671082
env.doc2path(env.docname))
@@ -1089,15 +1104,28 @@ def apply(self):
10891104
node.replace_self(xref)
10901105

10911106

1092-
class CreateSectionLabels(docutils.transforms.Transform):
1093-
"""Make labels for each document parsed by Sphinx, each section thereof,
1094-
and each Sphinx domain object.
1095-
1096-
These labels are referenced in ProcessLocalLinks.
1107+
class CreateNotebookSectionAnchors(docutils.transforms.Transform):
1108+
"""Create section anchors for Jupyter notebooks.
10971109
10981110
Note: Sphinx lower-cases the HTML section IDs, Jupyter doesn't.
1099-
This transform creates labels in the Jupyter style for Jupyter
1100-
notebooks, but keeps the Sphinx style for all other source files.
1111+
This transform creates anchors in the Jupyter style.
1112+
1113+
"""
1114+
1115+
default_priority = 200 # Before CreateSectionLabels (250)
1116+
1117+
def apply(self):
1118+
for section in self.document.traverse(docutils.nodes.section):
1119+
title = section.children[0].astext()
1120+
link_id = title.replace(' ', '-')
1121+
section['ids'] = [link_id]
1122+
1123+
1124+
class CreateSectionLabels(docutils.transforms.Transform):
1125+
"""Make labels for each document and each section thereof.
1126+
1127+
These labels are referenced in ProcessLocalLinks but can also be
1128+
used manually with ``:ref:``.
11011129
11021130
"""
11031131

@@ -1111,11 +1139,7 @@ def apply(self):
11111139
assert section.children
11121140
assert isinstance(section.children[0], docutils.nodes.title)
11131141
title = section.children[0].astext()
1114-
if file_ext.lower() == '.ipynb':
1115-
link_id = title.replace(' ', '-')
1116-
section['ids'] = [link_id]
1117-
else:
1118-
link_id = section['ids'][0]
1142+
link_id = section['ids'][0]
11191143
label = '/' + env.docname + file_ext + '#' + link_id
11201144
label = label.lower()
11211145
env.domaindata['std']['labels'][label] = (
@@ -1132,7 +1156,15 @@ def apply(self):
11321156
env.docname, '')
11331157
i_still_have_to_create_the_document_label = False
11341158

1135-
# Create labels for domain-specific object signatures
1159+
1160+
class CreateDomainObjectLabels(docutils.transforms.Transform):
1161+
"""Create labels for domain-specific object signatures."""
1162+
1163+
default_priority = 250 # About the same as CreateSectionLabels
1164+
1165+
def apply(self):
1166+
env = self.document.settings.env
1167+
file_ext = os.path.splitext(env.doc2path(env.docname))[1]
11361168
for sig in self.document.traverse(sphinx.addnodes.desc_signature):
11371169
try:
11381170
title = sig['ids'][0]
@@ -1420,6 +1452,8 @@ def setup(app):
14201452
app.connect('html-collect-pages', html_collect_pages)
14211453
app.connect('env-purge-doc', env_purge_doc)
14221454
app.add_transform(CreateSectionLabels)
1455+
app.add_transform(CreateDomainObjectLabels)
1456+
app.add_transform(ProcessLocalLinks)
14231457

14241458
# Make docutils' "code" directive (generated by markdown2rst/pandoc)
14251459
# behave like Sphinx's "code-block",

0 commit comments

Comments
 (0)