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
18class DocTreeTranslator(nodes.NodeVisitor, CommonSphinxWriterHelpers):
19 """
20 Defines a translator for doctree
21 """
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
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 = []
49 def log_unknown(self, type, node):
50 logger = logging.getLogger("DocTreeBuilder")
51 logger.warning(
52 "[doctree] %s(%s) unsupported formatting" % (type, node))
54 def wrap(self, text, width=STDINDENT):
55 self.wrapper.width = width
56 return self.wrapper.wrap(text)
58 def add_text(self, text, indent=-1):
59 self.states[-1].append((indent, text))
61 def new_state(self, indent=STDINDENT):
62 self.states.append([])
63 self.stateindent.append(indent)
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 = []
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))
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 = []
91 do_format()
92 self.states[-1].extend(result)
94 def visit_document(self, node):
95 self.new_state(0)
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)
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)
110 def depart_Text(self, node):
111 pass
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)
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()
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()
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
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")
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
174 # Function to convert the docname to a reST file name.
175 def file_transform(docname):
176 return docname + self.file_suffix
178 # Function to convert the docname to a relative URI.
179 def link_transform(docname):
180 return docname + self.link_suffix
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
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))
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
217 def get_target_uri(self, docname, typ=None):
218 return self.link_transform(docname)
220 def prepare_writing(self, docnames):
221 self.writer = DocTreeWriter(self)
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("\\", "/")
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)
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)
245 def finish(self):
246 pass
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
258 output = None
260 def __init__(self, builder):
261 writers.Writer.__init__(self)
262 self.builder = builder
264 def translate(self):
265 visitor = self.builder.create_translator(self.document, self.builder)
266 self.document.walkabout(visitor)
267 self.output = visitor.body
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')