Hot-keys on this page
r m x p toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
1# -*- coding: utf-8 -*-
2"""
3@file
4@brief Defines a :epkg:`sphinx` extension to display a link on github.
5"""
6import os
7import sphinx
8from docutils import nodes
9from docutils.parsers.rst.roles import set_classes
12class githublink_node(nodes.Element):
14 """
15 defines *githublink* node
16 """
17 pass
20def make_link_node(rawtext, app, path, anchor, lineno, options, settings):
21 """
22 Create a link to a github file.
24 :param rawtext: Text being replaced with link node.
25 :param app: Sphinx application context
26 :param path: path to filename
27 :param lineno: line number
28 :param anchor: anchor
29 :param options: Options dictionary passed to role func.
30 :param settings: settings
32 The configuration of the documentation must contain
33 a member ``githublink_options`` (a dictionary) which contains the following fields
34 either the pair:
36 * *user*, *project*: to form the url
37 ``https://github.com/<user>/<project>``
39 Or the field:
41 * *processor*: function with takes a path and a line number and returns an url and an anchor name
43 Example:
45 ::
47 def processor_github(path, lineno):
48 url = "https://github.com/{0}/{1}/blob/master/{2}".format(user, project, path)
49 if lineno:
50 url += "#L{0}".format(lineno)
51 return url, "source on GitHub"
52 """
53 try:
54 exc = []
55 try:
56 config = app.config
57 except AttributeError as e: # pragma: no cover
58 exc.append(e)
59 config = None
60 if config is not None:
61 try:
62 opt = config.githublink_options
63 except AttributeError as ee: # pragma: no cover
64 exc.append(ee)
65 opt = None
66 else:
67 opt = None # pragma: no cover
68 if not opt:
69 try:
70 opt = settings.githublink_options
71 except AttributeError as eee:
72 exc.append(eee)
73 opt = None
74 if not opt:
75 lines = "\n".join("## {0} ##".format(str(e)) for e in exc)
76 raise AttributeError(
77 "settings does not have a key githublink_options, app does not have a member config.\n{0}".format(lines))
78 except AttributeError:
79 # it just means the role will be ignored
80 return None
81 if "processor" not in opt:
82 user = opt["user"]
83 project = opt["project"]
84 ll = 'x' if '.cpython' in path else ''
85 ref = "https://github.com/{0}/{1}/blob/master/{2}{3}".format(
86 user, project, path, ll)
87 if lineno:
88 ref += "#L{0}".format(lineno)
89 else:
90 ref, anchor = opt["processor"](path, lineno)
91 if anchor == "%" and 'anchor' in opt:
92 anchor = opt['anchor']
93 set_classes(options)
94 node = nodes.reference(rawtext, anchor, refuri=ref, **options)
95 return node
98def githublink_role(role, rawtext, text, lineno, inliner,
99 options=None, content=None):
100 """
101 Defines custom role *githublink*. The following instruction add
102 a link to the documentation on github.
104 :githublink:`source on GitHub|py`
106 :param role: The role name used in the document.
107 :param rawtext: The entire markup snippet, with role.
108 :param text: The text marked with the role.
109 :param lineno: The line number where rawtext appears in the input.
110 :param inliner: The inliner instance that called us.
111 :param options: Directive options for customization (dictionary)
112 :param content: The directive content for customization (list)
113 :return: ``[node], []``
115 The pipe ``|`` indicates that an extension must be added to
116 *docname* to get the true url.
118 Different formats handled by the role:
120 * ``anchor``: anchor = filename, line number is guess form the position in the file
121 * ``anchor|py|*``: extension *.py* is added to the anchor, no line number
122 * ``anchor|py|45``: extension *.py* is added to the anchor, line number is 45
123 * ``%|py|45``: the anchor name comes from the variable ``githublink_options['anchor']`` in the configuration file.
125 A suffix can be added to the extension ``rst-doc`` to tell the extension
126 the source comes from the subfolder ``_doc/sphinx/source`` and not from
127 a subfolder like ``src``.
128 """
129 if options is None:
130 options = {}
131 if content is None:
132 content = []
133 if not rawtext or len(rawtext) == 0:
134 rawtext = "source" # pragma: no cover
136 app = inliner.document.settings.env.app
137 docname = inliner.document.settings.env.docname
138 folder = docname
140 # Retrieves extension and path.
141 text0 = text
142 path = None
143 if "|" in text:
144 # No extension to the url, it adds one.
145 spl = text.split("|")
146 if len(spl) == 3:
147 text, ext, no = spl
148 if len(ext) > 7 or "." in ext:
149 path = ext
150 ext = None
151 else:
152 ext = "." + ext
153 lineno = int(no) if no != "*" else None
154 elif len(spl) != 2:
155 raise ValueError( # pragma: no cover
156 "Unable to interpret '{0}'.".format(text))
157 else:
158 text, ext = spl
159 ext = "." + ext
160 else:
161 ext = None
163 # -
164 if ext is not None and "-" in ext:
165 spl = ext.split("-")
166 if len(spl) != 2:
167 raise ValueError( # pragma: no cover
168 "Unable to interpret extension in '{0}'".format(text0))
169 ext, doc = spl
170 else:
171 doc = "src"
173 # Get path to source.
174 if path is None:
175 git = os.path.join(folder, ".git")
176 while len(folder) > 0 and not os.path.exists(git):
177 folder = os.path.split(folder)[0]
178 git = os.path.join(folder, ".git")
180 if len(folder) > 0:
181 path = docname[len(folder):]
182 elif doc == "src":
183 path = docname
184 source_doc = inliner.document.settings._source
185 if source_doc is not None:
186 source_doc = source_doc.replace("\\", "/")
187 spl = source_doc.split('/')
188 if '_doc' in spl:
189 sub_doc = spl[:spl.index('_doc')]
190 root_doc = "/".join(sub_doc)
191 root_doc_src = os.path.join(root_doc, 'src')
192 if os.path.exists(root_doc_src):
193 path = os.path.join('src', docname) # pragma: no cover
194 elif doc == "doc":
195 path = os.path.join('_doc', 'sphinxdoc', 'source', docname)
196 else:
197 raise ValueError( # pragma: no cover
198 "Unable to interpret subfolder in '{0}'.".format(text0))
200 # Path with extension.
201 if ext is not None:
202 path += ext
203 path = path.replace("\\", "/")
205 # Get rid of binaries (.pyd, .so) --> add a link to the root.
206 if path.endswith(".pyd") or path.endswith(".so"):
207 path = "/".join(path.split("/")[:-1]).rstrip('/') + '/'
209 # Add node.
210 try:
211 node = make_link_node(rawtext=rawtext, app=app, path=path, lineno=lineno,
212 options=options, anchor=text, settings=inliner.document.settings)
213 except (ValueError, AttributeError) as e: # pragma: no cover
214 msg = inliner.reporter.error(
215 'githublink_options must be set to a dictionary with keys '
216 '(user, project)\n%s' % str(e), line=lineno)
217 prb = inliner.problematic(rawtext, rawtext, msg)
218 return [prb], [msg]
219 if node is None:
220 return [], []
221 return [node], []
224def setup(app):
225 """
226 setup for ``githublink`` (:epkg:`sphinx`)
227 """
228 app.add_role('githublink', githublink_role)
229 app.add_role('gitlink', githublink_role)
230 app.add_config_value('githublink_options', None, 'env')
231 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}