Coverage for pyquickhelper/sphinxext/sphinx_postcontents_extension.py: 96%

115 statements  

« prev     ^ index     » next       coverage.py v7.2.7, created at 2023-06-03 02:21 +0200

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(f"postid-{id(subnode)}") 

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

136 if nid in memo: 

137 raise KeyError( # pragma: no cover 

138 f"node was already added '{nid}'") 

139 logger.info("[%r] %ssection id %r", event, ' ' * level, nid) 

140 level += 1 

141 memo[nid] = subnode 

142 bli = nodes.bullet_list() 

143 roots[-1] += bli 

144 roots.append(bli) 

145 sections.append(subnode) 

146 elif isinstance(subnode, nodes.title): 

147 logger.info("[%r] %stitle %r", event, 

148 ' ' * level, subnode.astext()) 

149 par = nodes.paragraph() 

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

151 reftitle=subnode.astext(), 

152 text=subnode.astext()) 

153 par += ref 

154 bullet = nodes.list_item() 

155 bullet += par 

156 roots[-1] += bullet 

157 elif isinstance(subnode, NodeLeave): 

158 parent = subnode.parent 

159 if isinstance(parent, nodes.section): 

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

161 if ids in memo: 

162 level -= 1 

163 logger.info("[%r] %send of section %r", 

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

165 sections.pop() 

166 roots.pop() 

167 

168 

169def transform_postcontents(app, doctree, fromdocname): 

170 """ 

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

172 every section in page stored in *postcontents_all_postcontents* 

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

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

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

176 

177 @param app Sphinx application 

178 @param doctree doctree 

179 @param fromdocname docname 

180 

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

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

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

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

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

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

187 """ 

188 logger = logging.getLogger("postcontents") 

189 

190 # check this is something to process 

191 env = app.builder.env 

192 attr_name = 'postcontents_all_postcontents' 

193 if not hasattr(env, attr_name): 

194 setattr(env, attr_name, []) 

195 post_list = getattr(env, attr_name) 

196 if len(post_list) == 0: 

197 # No postcontents found. 

198 return 

199 

200 for node in post_list: 

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

202 logger.warning("[postcontents] no first loop was ever processed: 'pcprocessed'=%s , File %r, line %r", 

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

204 continue 

205 if len(node.children) > 0: 

206 # already processed 

207 continue 

208 

209 _modify_postcontents(node, "postcontentsT") 

210 

211 

212def visit_postcontents_node(self, node): 

213 """ 

214 does nothing 

215 """ 

216 pass 

217 

218 

219def depart_postcontents_node(self, node): 

220 """ 

221 does nothing 

222 """ 

223 pass 

224 

225 

226def setup(app): 

227 """ 

228 setup for ``postcontents`` (sphinx) 

229 """ 

230 if hasattr(app, "add_mapping"): 

231 app.add_mapping('postcontents', postcontents_node) 

232 

233 app.add_node(postcontents_node, 

234 html=(visit_postcontents_node, depart_postcontents_node), 

235 epub=(visit_postcontents_node, depart_postcontents_node), 

236 elatex=(visit_postcontents_node, depart_postcontents_node), 

237 latex=(visit_postcontents_node, depart_postcontents_node), 

238 text=(visit_postcontents_node, depart_postcontents_node), 

239 md=(visit_postcontents_node, depart_postcontents_node), 

240 rst=(visit_postcontents_node, depart_postcontents_node)) 

241 

242 app.add_directive('postcontents', PostContentsDirective) 

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

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

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