Hide keyboard shortcuts

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 

10 

11 

12class githublink_node(nodes.Element): 

13 

14 """ 

15 defines *githublink* node 

16 """ 

17 pass 

18 

19 

20def make_link_node(rawtext, app, path, anchor, lineno, options, settings): 

21 """ 

22 Create a link to a github file. 

23 

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 

31 

32 The configuration of the documentation must contain 

33 a member ``githublink_options`` (a dictionary) which contains the following fields 

34 either the pair: 

35 

36 * *user*, *project*: to form the url 

37 ``https://github.com/<user>/<project>`` 

38 

39 Or the field: 

40 

41 * *processor*: function with takes a path and a line number and returns an url and an anchor name 

42 

43 Example: 

44 

45 :: 

46 

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 

96 

97 

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. 

103 

104 :githublink:`source on GitHub|py` 

105 

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], []`` 

114 

115 The pipe ``|`` indicates that an extension must be added to 

116 *docname* to get the true url. 

117 

118 Different formats handled by the role: 

119 

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. 

124 

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 

135 

136 app = inliner.document.settings.env.app 

137 docname = inliner.document.settings.env.docname 

138 folder = docname 

139 

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 

162 

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" 

172 

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") 

179 

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)) 

199 

200 # Path with extension. 

201 if ext is not None: 

202 path += ext 

203 path = path.replace("\\", "/") 

204 

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('/') + '/' 

208 

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], [] 

222 

223 

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}