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 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 "Unexpected type for builder {0}".format(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( 

52 "[doctree] %s(%s) unsupported formatting" % (type, node)) 

53 

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

55 self.wrapper.width = width 

56 return self.wrapper.wrap(text) 

57 

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

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

60 

61 def new_state(self, indent=STDINDENT): 

62 self.states.append([]) 

63 self.stateindent.append(indent) 

64 

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

66 content = self.states.pop() 

67 maxindent = sum(self.stateindent) 

68 indent = self.stateindent.pop() 

69 result = [] 

70 toformat = [] 

71 

72 def do_format(): 

73 if not toformat: 

74 return 

75 if wrap: 

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

77 else: 

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

79 if end: 

80 res += end 

81 result.append((indent, res)) 

82 

83 for itemindent, item in content: 

84 if itemindent == -1: 

85 toformat.append(item) 

86 else: 

87 do_format() 

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

89 toformat = [] 

90 

91 do_format() 

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

93 

94 def visit_document(self, node): 

95 self.new_state(0) 

96 

97 def depart_document(self, node): 

98 self.end_state() 

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

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

101 for line in lines) 

102 

103 def visit_Text(self, node): 

104 text = node.astext() 

105 if self.inline: 

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

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

108 self.add_text(text) 

109 

110 def depart_Text(self, node): 

111 pass 

112 

113 def _format_obj(self, obj): 

114 if isinstance(obj, str): 

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

116 elif isinstance(obj, nodes.Node): 

117 return "<node={0}[...]>".format(obj.__class__.__name__) 

118 else: 

119 return str(obj) 

120 

121 def unknown_visit(self, node): 

122 self.new_state(0) 

123 self.add_text("<{0}".format(node.__class__.__name__)) 

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

125 res = ['{0}={1}'.format(k, self._format_obj(v)) 

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

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

128 if res: 

129 if self.inline: 

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

131 else: 

132 for kv in res: 

133 self.new_state() 

134 self.add_text("- " + kv) 

135 self.add_text(self.nl) 

136 self.end_state() 

137 self.add_text(">") 

138 self.new_state() 

139 

140 def unknown_departure(self, node): 

141 self.end_state(wrap=self.dowrap) 

142 self.add_text("</{0}>".format(node.__class__.__name__)) 

143 self.end_state() 

144 

145 

146class DocTreeBuilder(Builder): 

147 """ 

148 Defines a doctree builder. 

149 """ 

150 name = 'doctree' 

151 format = 'doctree' 

152 file_suffix = '.doctree.txt' 

153 link_suffix = None 

154 default_translator_class = DocTreeTranslator 

155 

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

157 """ 

158 Constructor, add a logger. 

159 """ 

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

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

162 

163 def init(self): 

164 """ 

165 Load necessary templates and perform initialization. 

166 """ 

167 if self.config.doctree_file_suffix is not None: 

168 self.file_suffix = self.config.doctree_file_suffix 

169 if self.config.doctree_link_suffix is not None: 

170 self.link_suffix = self.config.doctree_link_suffix 

171 if self.link_suffix is None: 

172 self.link_suffix = self.file_suffix 

173 

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

175 def file_transform(docname): 

176 return docname + self.file_suffix 

177 

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

179 def link_transform(docname): 

180 return docname + self.link_suffix 

181 

182 if self.config.doctree_file_transform is not None: 

183 self.file_transform = self.config.doctree_file_transform 

184 else: 

185 self.file_transform = file_transform 

186 if self.config.doctree_link_transform is not None: 

187 self.link_transform = self.config.doctree_link_transform 

188 else: 

189 self.link_transform = link_transform 

190 

191 def get_outdated_docs(self): 

192 """ 

193 Return an iterable of input files that are outdated. 

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

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

196 """ 

197 for docname in self.env.found_docs: 

198 if docname not in self.env.all_docs: 

199 yield docname 

200 continue 

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

202 self.file_suffix) 

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

204 

205 try: 

206 targetmtime = path.getmtime(targetname) 

207 except Exception: 

208 targetmtime = 0 

209 try: 

210 srcmtime = path.getmtime(sourcename) 

211 if srcmtime > targetmtime: 

212 yield docname 

213 except EnvironmentError: 

214 # source doesn't exist anymore 

215 pass 

216 

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

218 return self.link_transform(docname) 

219 

220 def prepare_writing(self, docnames): 

221 self.writer = DocTreeWriter(self) 

222 

223 def get_outfilename(self, pagename): 

224 """ 

225 Overwrites *get_target_uri* to control file names. 

226 """ 

227 return "{0}/{1}.doctree.txt".format(self.outdir, pagename).replace("\\", "/") 

228 

229 def write_doc(self, docname, doctree): 

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

231 self.current_docname = docname 

232 self.writer.write(doctree, destination) 

233 ctx = None 

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

235 

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

237 outfilename=None, event_arg=None): 

238 if templatename is not None: 

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

240 outfilename = self.get_outfilename(pagename) 

241 ensuredir(path.dirname(outfilename)) 

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

243 f.write(self.writer.output) 

244 

245 def finish(self): 

246 pass 

247 

248 

249class DocTreeWriter(writers.Writer): 

250 """ 

251 Defines a doctree writer. 

252 """ 

253 supported = ('text',) 

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

255 settings_defaults = {} 

256 translator_class = DocTreeTranslator 

257 

258 output = None 

259 

260 def __init__(self, builder): 

261 writers.Writer.__init__(self) 

262 self.builder = builder 

263 

264 def translate(self): 

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

266 self.document.walkabout(visitor) 

267 self.output = visitor.body 

268 

269 

270def setup(app): 

271 """ 

272 Initializes the doctree builder. 

273 """ 

274 app.add_builder(DocTreeBuilder) 

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

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

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

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

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

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

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

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