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 which proposes a new version of ``.. contents::`` 

5which takes into account titles dynamically added. 

6""" 

7from docutils import nodes 

8from docutils.parsers.rst import directives 

9from sphinx.util import logging 

10 

11import sphinx 

12from sphinx.util.logging import getLogger 

13from docutils.parsers.rst import Directive 

14from .sphinx_ext_helper import traverse, NodeLeave, WrappedNode 

15 

16 

17class postcontents_node(nodes.paragraph): 

18 """ 

19 defines ``postcontents`` node 

20 """ 

21 pass 

22 

23 

24class PostContentsDirective(Directive): 

25 """ 

26 Defines a sphinx extension which proposes a new version of ``.. contents::`` 

27 which takes into account titles dynamically added. 

28 

29 Example:: 

30 

31 .. postcontents:: 

32 

33 title 1 

34 ======= 

35 

36 .. runpython:: 

37 :rst: 

38 

39 print("title 2") 

40 print("=======") 

41 

42 Which renders as: 

43 

44 .. contents:: 

45 :local: 

46 

47 title 1 

48 ======= 

49 

50 title 2 

51 ======= 

52 

53 Directive ``.. contents::`` only considers titles defined by the user, 

54 not titles dynamically created by another directives. 

55 

56 .. warning:: It is not recommended to dynamically insert 

57 such a directive. It is not recursive. 

58 """ 

59 

60 node_class = postcontents_node 

61 name_sphinx = "postcontents" 

62 has_content = True 

63 option_spec = {'depth': directives.unchanged, 

64 'local': directives.unchanged} 

65 

66 def run(self): 

67 """ 

68 Just add a @see cl postcontents_node. 

69 

70 @return list of nodes or list of nodes, container 

71 """ 

72 lineno = self.lineno 

73 

74 settings = self.state.document.settings 

75 env = settings.env if hasattr(settings, "env") else None 

76 docname = None if env is None else env.docname 

77 if docname is not None: 

78 docname = docname.replace("\\", "/").split("/")[-1] 

79 else: 

80 docname = '' # pragma: no cover 

81 

82 node = postcontents_node() 

83 node['pclineno'] = lineno 

84 node['pcdocname'] = docname 

85 node["pcprocessed"] = 0 

86 node["depth"] = self.options.get("depth", "*") 

87 node["local"] = self.options.get("local", None) 

88 return [node] 

89 

90 

91def process_postcontents(app, doctree): 

92 """ 

93 Collect all *postcontents* in the environment. 

94 Look for the section or document which contain them. 

95 Put them into the variable *postcontents_all_postcontents* in the config. 

96 """ 

97 logger = getLogger('postcontents') 

98 env = app.builder.env 

99 attr = 'postcontents_all_postcontents' 

100 if not hasattr(env, attr): 

101 setattr(env, attr, []) 

102 attr_list = getattr(env, attr) 

103 for node in doctree.traverse(postcontents_node): 

104 # It looks for a section or document which contains the directive. 

105 parent = node 

106 while not isinstance(parent, (nodes.document, nodes.section)): 

107 parent = node.parent 

108 node["node_section"] = WrappedNode(parent) 

109 node["pcprocessed"] += 1 

110 node["processed"] = 1 

111 attr_list.append(node) 

112 logger.info("[postcontents] in '{}.rst' line={} found:{}".format( 

113 node['pcdocname'], node['pclineno'], node['pcprocessed'])) 

114 _modify_postcontents(node, "postcontentsP") 

115 

116 

117def _modify_postcontents(node, event): 

118 node["transformed"] = 1 

119 logger = getLogger('postcontents') 

120 logger.info("[{}] in '{}.rst' line={} found:{}".format( 

121 event, node['pcdocname'], node['pclineno'], node['pcprocessed'])) 

122 parent = node["node_section"] 

123 sections = [] 

124 main_par = nodes.paragraph() 

125 node += main_par 

126 roots = [main_par] 

127 # depth = int(node["depth"]) if node["depth"] != '*' else 20 

128 memo = {} 

129 level = 0 

130 

131 for _, subnode in traverse(parent): 

132 if isinstance(subnode, nodes.section): 

133 if len(subnode["ids"]) == 0: 

134 subnode["ids"].append("postid-{}".format(id(subnode))) 

135 nid = subnode["ids"][0] 

136 if nid in memo: 

137 raise KeyError( # pragma: no cover 

138 "node was already added '{0}'".format(nid)) 

139 logger.info("[{}] {}section id '{}'".format( 

140 event, " " * level, nid)) 

141 level += 1 

142 memo[nid] = subnode 

143 bli = nodes.bullet_list() 

144 roots[-1] += bli 

145 roots.append(bli) 

146 sections.append(subnode) 

147 elif isinstance(subnode, nodes.title): 

148 logger.info("[{}] {}title '{}'".format( 

149 event, " " * level, subnode.astext())) 

150 par = nodes.paragraph() 

151 ref = nodes.reference(refid=sections[-1]["ids"][0], 

152 reftitle=subnode.astext(), 

153 text=subnode.astext()) 

154 par += ref 

155 bullet = nodes.list_item() 

156 bullet += par 

157 roots[-1] += bullet 

158 elif isinstance(subnode, NodeLeave): 

159 parent = subnode.parent 

160 if isinstance(parent, nodes.section): 

161 ids = None if len(parent["ids"]) == 0 else parent["ids"][0] 

162 if ids in memo: 

163 level -= 1 

164 logger.info("[{}] {}end of section '{}'".format( 

165 event, " " * level, parent["ids"])) 

166 sections.pop() 

167 roots.pop() 

168 

169 

170def transform_postcontents(app, doctree, fromdocname): 

171 """ 

172 The function is called by event ``'doctree_resolved'``. It looks for 

173 every section in page stored in *postcontents_all_postcontents* 

174 in the configuration and builds a short table of contents. 

175 The instruction ``.. contents::`` is resolved before every directive in 

176 the page is executed, the instruction ``.. postcontents::`` is resolved after. 

177 

178 @param app Sphinx application 

179 @param doctree doctree 

180 @param fromdocname docname 

181 

182 Thiis directive should be used if you need to capture a section 

183 which was dynamically added by another one. For example @see cl RunPythonDirective 

184 calls function ``nested_parse_with_titles``. ``.. postcontents::`` will capture the 

185 new section this function might eventually add to the page. 

186 For some reason, this function does not seem to be able to change 

187 the doctree (any creation of nodes is not taken into account). 

188 """ 

189 logger = logging.getLogger("postcontents") 

190 

191 # check this is something to process 

192 env = app.builder.env 

193 attr_name = 'postcontents_all_postcontents' 

194 if not hasattr(env, attr_name): 

195 setattr(env, attr_name, []) 

196 post_list = getattr(env, attr_name) 

197 if len(post_list) == 0: 

198 # No postcontents found. 

199 return 

200 

201 for node in post_list: 

202 if node["pcprocessed"] != 1: 

203 logger.warning("[postcontents] no first loop was ever processed: 'pcprocessed'={0} , File '{1}', line {2}".format( 

204 node["pcprocessed"], node["pcdocname"], node["pclineno"])) 

205 continue 

206 if len(node.children) > 0: 

207 # already processed 

208 continue 

209 

210 _modify_postcontents(node, "postcontentsT") 

211 

212 

213def visit_postcontents_node(self, node): 

214 """ 

215 does nothing 

216 """ 

217 pass 

218 

219 

220def depart_postcontents_node(self, node): 

221 """ 

222 does nothing 

223 """ 

224 pass 

225 

226 

227def setup(app): 

228 """ 

229 setup for ``postcontents`` (sphinx) 

230 """ 

231 if hasattr(app, "add_mapping"): 

232 app.add_mapping('postcontents', postcontents_node) 

233 

234 app.add_node(postcontents_node, 

235 html=(visit_postcontents_node, depart_postcontents_node), 

236 epub=(visit_postcontents_node, depart_postcontents_node), 

237 elatex=(visit_postcontents_node, depart_postcontents_node), 

238 latex=(visit_postcontents_node, depart_postcontents_node), 

239 text=(visit_postcontents_node, depart_postcontents_node), 

240 md=(visit_postcontents_node, depart_postcontents_node), 

241 rst=(visit_postcontents_node, depart_postcontents_node)) 

242 

243 app.add_directive('postcontents', PostContentsDirective) 

244 app.connect('doctree-read', process_postcontents) 

245 app.connect('doctree-resolved', transform_postcontents) 

246 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}