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
« 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
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 f"Unexpected type for builder {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("[doctree] %s(%s) unsupported formatting", type, node)
53 def wrap(self, text, width=STDINDENT):
54 self.wrapper.width = width
55 return self.wrapper.wrap(text)
57 def add_text(self, text, indent=-1):
58 self.states[-1].append((indent, text))
60 def new_state(self, indent=STDINDENT):
61 self.states.append([])
62 self.stateindent.append(indent)
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 = []
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))
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 = []
90 do_format()
91 self.states[-1].extend(result)
93 def visit_document(self, node):
94 self.new_state(0)
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)
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)
109 def depart_Text(self, node):
110 pass
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)
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()
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()
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
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")
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
173 # Function to convert the docname to a reST file name.
174 def file_transform(docname):
175 return docname + self.file_suffix
177 # Function to convert the docname to a relative URI.
178 def link_transform(docname):
179 return docname + self.link_suffix
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
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))
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
216 def get_target_uri(self, docname, typ=None):
217 return self.link_transform(docname)
219 def prepare_writing(self, docnames):
220 self.writer = DocTreeWriter(self)
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("\\", "/")
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)
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)
244 def finish(self):
245 pass
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
257 output = None
259 def __init__(self, builder):
260 writers.Writer.__init__(self)
261 self.builder = builder
263 def translate(self):
264 visitor = self.builder.create_translator(self.document, self.builder)
265 self.document.walkabout(visitor)
266 self.output = visitor.body
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')