Coverage for pyquickhelper/sphinxext/sphinx_doctree_builder.py: 77%

188 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 sphinx extension to output :epkg:`sphinx` doctree. 

5""" 

6import os 

7import textwrap 

8from os import path 

9from sphinx.util import logging 

10from docutils.io import StringOutput 

11from sphinx.builders import Builder 

12from sphinx.util.osutil import ensuredir 

13from docutils import nodes, writers 

14from sphinx.writers.text import MAXWIDTH, STDINDENT 

15from ._sphinx_common_builder import CommonSphinxWriterHelpers 

16 

17 

18class DocTreeTranslator(nodes.NodeVisitor, CommonSphinxWriterHelpers): 

19 """ 

20 Defines a translator for doctree 

21 """ 

22 

23 def __init__(self, document, builder): 

24 if not hasattr(builder, 'config'): 

25 raise TypeError( # pragma: no cover 

26 f"Unexpected type for builder {type(builder)}") 

27 nodes.NodeVisitor.__init__(self, document) 

28 self.builder = builder 

29 

30 newlines = builder.config.text_newlines 

31 if newlines == 'windows': 

32 self.nl = '\r\n' 

33 elif newlines == 'native': 

34 self.nl = os.linesep 

35 else: 

36 self.nl = '\n' 

37 self.states = [[]] 

38 self.stateindent = [0] 

39 if self.builder.config.doctree_indent: 

40 self.indent = self.builder.config.doctree_indent 

41 else: 

42 self.indent = STDINDENT 

43 self.wrapper = textwrap.TextWrapper( 

44 width=STDINDENT, break_long_words=False, break_on_hyphens=False) 

45 self.dowrap = self.builder.config.doctree_wrap 

46 self.inline = self.builder.config.doctree_inline 

47 self._table = [] 

48 

49 def log_unknown(self, type, node): 

50 logger = logging.getLogger("DocTreeBuilder") 

51 logger.warning("[doctree] %s(%s) unsupported formatting", type, node) 

52 

53 def wrap(self, text, width=STDINDENT): 

54 self.wrapper.width = width 

55 return self.wrapper.wrap(text) 

56 

57 def add_text(self, text, indent=-1): 

58 self.states[-1].append((indent, text)) 

59 

60 def new_state(self, indent=STDINDENT): 

61 self.states.append([]) 

62 self.stateindent.append(indent) 

63 

64 def end_state(self, wrap=False, end=None): 

65 content = self.states.pop() 

66 maxindent = sum(self.stateindent) 

67 indent = self.stateindent.pop() 

68 result = [] 

69 toformat = [] 

70 

71 def do_format(): 

72 if not toformat: 

73 return 

74 if wrap: 

75 res = self.wrap(''.join(toformat), width=MAXWIDTH - maxindent) 

76 else: 

77 res = ''.join(toformat).splitlines() 

78 if end: 

79 res += end 

80 result.append((indent, res)) 

81 

82 for itemindent, item in content: 

83 if itemindent == -1: 

84 toformat.append(item) 

85 else: 

86 do_format() 

87 result.append((indent + itemindent, item)) 

88 toformat = [] 

89 

90 do_format() 

91 self.states[-1].extend(result) 

92 

93 def visit_document(self, node): 

94 self.new_state(0) 

95 

96 def depart_document(self, node): 

97 self.end_state() 

98 self.body = self.nl.join(line and (' ' * indent + line) 

99 for indent, lines in self.states[0] 

100 for line in lines) 

101 

102 def visit_Text(self, node): 

103 text = node.astext() 

104 if self.inline: 

105 text = text.replace("\n", "\\n").replace( 

106 "\r", "").replace("\t", "\\t") 

107 self.add_text(text) 

108 

109 def depart_Text(self, node): 

110 pass 

111 

112 def _format_obj(self, obj): 

113 if isinstance(obj, str): 

114 return "'{0}'".format(obj.replace("'", "\\'")) 

115 elif isinstance(obj, nodes.Node): 

116 return f"<node={obj.__class__.__name__}[...]>" 

117 else: 

118 return str(obj) 

119 

120 def unknown_visit(self, node): 

121 self.new_state(0) 

122 self.add_text(f"<{node.__class__.__name__}") 

123 if hasattr(node, 'attributes') and node.attributes: 

124 res = [f'{k}={self._format_obj(v)}' 

125 for k, v in sorted(node.attributes.items()) 

126 if v not in (None, [], '')] 

127 if res: 

128 if self.inline: 

129 self.add_text(" " + " ".join(res)) 

130 else: 

131 for kv in res: 

132 self.new_state() 

133 self.add_text("- " + kv) 

134 self.add_text(self.nl) 

135 self.end_state() 

136 self.add_text(">") 

137 self.new_state() 

138 

139 def unknown_departure(self, node): 

140 self.end_state(wrap=self.dowrap) 

141 self.add_text(f"</{node.__class__.__name__}>") 

142 self.end_state() 

143 

144 

145class DocTreeBuilder(Builder): 

146 """ 

147 Defines a doctree builder. 

148 """ 

149 name = 'doctree' 

150 format = 'doctree' 

151 file_suffix = '.doctree.txt' 

152 link_suffix = None 

153 default_translator_class = DocTreeTranslator 

154 

155 def __init__(self, *args, **kwargs): 

156 """ 

157 Constructor, add a logger. 

158 """ 

159 Builder.__init__(self, *args, **kwargs) 

160 self.logger = logging.getLogger("DocTreeBuilder") 

161 

162 def init(self): 

163 """ 

164 Load necessary templates and perform initialization. 

165 """ 

166 if self.config.doctree_file_suffix is not None: 

167 self.file_suffix = self.config.doctree_file_suffix 

168 if self.config.doctree_link_suffix is not None: 

169 self.link_suffix = self.config.doctree_link_suffix 

170 if self.link_suffix is None: 

171 self.link_suffix = self.file_suffix 

172 

173 # Function to convert the docname to a reST file name. 

174 def file_transform(docname): 

175 return docname + self.file_suffix 

176 

177 # Function to convert the docname to a relative URI. 

178 def link_transform(docname): 

179 return docname + self.link_suffix 

180 

181 if self.config.doctree_file_transform is not None: 

182 self.file_transform = self.config.doctree_file_transform 

183 else: 

184 self.file_transform = file_transform 

185 if self.config.doctree_link_transform is not None: 

186 self.link_transform = self.config.doctree_link_transform 

187 else: 

188 self.link_transform = link_transform 

189 

190 def get_outdated_docs(self): 

191 """ 

192 Return an iterable of input files that are outdated. 

193 This method is taken from ``TextBuilder.get_outdated_docs()`` 

194 with minor changes to support ``(confval, doctree_file_transform))``. 

195 """ 

196 for docname in self.env.found_docs: 

197 if docname not in self.env.all_docs: 

198 yield docname 

199 continue 

200 sourcename = path.join(self.env.srcdir, docname + 

201 self.file_suffix) 

202 targetname = path.join(self.outdir, self.file_transform(docname)) 

203 

204 try: 

205 targetmtime = path.getmtime(targetname) 

206 except Exception: 

207 targetmtime = 0 

208 try: 

209 srcmtime = path.getmtime(sourcename) 

210 if srcmtime > targetmtime: 

211 yield docname 

212 except EnvironmentError: 

213 # source doesn't exist anymore 

214 pass 

215 

216 def get_target_uri(self, docname, typ=None): 

217 return self.link_transform(docname) 

218 

219 def prepare_writing(self, docnames): 

220 self.writer = DocTreeWriter(self) 

221 

222 def get_outfilename(self, pagename): 

223 """ 

224 Overwrites *get_target_uri* to control file names. 

225 """ 

226 return f"{self.outdir}/{pagename}.doctree.txt".replace("\\", "/") 

227 

228 def write_doc(self, docname, doctree): 

229 destination = StringOutput(encoding='utf-8') 

230 self.current_docname = docname 

231 self.writer.write(doctree, destination) 

232 ctx = None 

233 self.handle_page(docname, ctx, event_arg=doctree) 

234 

235 def handle_page(self, pagename, addctx, templatename=None, 

236 outfilename=None, event_arg=None): 

237 if templatename is not None: 

238 raise NotImplementedError("templatename must be None.") 

239 outfilename = self.get_outfilename(pagename) 

240 ensuredir(path.dirname(outfilename)) 

241 with open(outfilename, 'w', encoding='utf-8') as f: 

242 f.write(self.writer.output) 

243 

244 def finish(self): 

245 pass 

246 

247 

248class DocTreeWriter(writers.Writer): 

249 """ 

250 Defines a doctree writer. 

251 """ 

252 supported = ('text',) 

253 settings_spec = ('No options here.', '', ()) 

254 settings_defaults = {} 

255 translator_class = DocTreeTranslator 

256 

257 output = None 

258 

259 def __init__(self, builder): 

260 writers.Writer.__init__(self) 

261 self.builder = builder 

262 

263 def translate(self): 

264 visitor = self.builder.create_translator(self.document, self.builder) 

265 self.document.walkabout(visitor) 

266 self.output = visitor.body 

267 

268 

269def setup(app): 

270 """ 

271 Initializes the doctree builder. 

272 """ 

273 app.add_builder(DocTreeBuilder) 

274 app.add_config_value('doctree_file_suffix', ".doctree.txt", 'env') 

275 app.add_config_value('doctree_link_suffix', None, 'env') 

276 app.add_config_value('doctree_file_transform', None, 'env') 

277 app.add_config_value('doctree_link_transform', None, 'env') 

278 app.add_config_value('doctree_indent', STDINDENT, 'env') 

279 app.add_config_value('doctree_image_dest', None, 'env') 

280 app.add_config_value('doctree_wrap', False, 'env') 

281 app.add_config_value('doctree_inline', True, 'env')