Coverage for pyquickhelper/sphinxext/sphinx_blog_extension.py: 81%
186 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 blogpost directives.
5See `Tutorial: Writing a simple extension
6<https://www.sphinx-doc.org/en/master/development/tutorials/helloworld.html>`_,
7`Creating reStructuredText Directives
8<https://docutils.sourceforge.io/docs/howto/rst-directives.html>`_
9"""
10import os
11import sphinx
12from docutils import nodes
13from docutils.parsers.rst import Directive
14from sphinx.locale import _ as _locale
15from docutils.parsers.rst import directives
16from docutils.statemachine import StringList
17from sphinx import addnodes
18from sphinx.util.nodes import set_source_info, process_index_entry
19from sphinx.util.nodes import nested_parse_with_titles
20from .blog_post import BlogPost
21from ..texthelper.texts_language import TITLES
24class blogpost_node(nodes.Element):
26 """
27 Defines *blogpost* node.
28 """
29 pass
32class blogpostagg_node(nodes.Element):
34 """
35 Defines *blogpostagg* node.
36 """
37 pass
40class BlogPostDirective(Directive):
42 """
43 Extracts information about a blog post described by a directive ``.. blogpost::``
44 and modifies the documentation if *env* is not null. The directive handles the following
45 options:
47 * *date*: date of the blog (mandatory)
48 * *title*: title (mandatory)
49 * *keywords*: keywords, comma separated (mandatory)
50 * *categories*: categories, comma separated (mandatory)
51 * *author*: author (optional)
52 * *blog_background*: can change the blog background (boolean, default is True)
53 * *lid* or *label*: an id to refer to (optional)
54 """
55 required_arguments = 0
56 optional_arguments = 0
57 final_argument_whitespace = True
58 option_spec = {'date': directives.unchanged,
59 'title': directives.unchanged,
60 'keywords': directives.unchanged,
61 'categories': directives.unchanged,
62 'author': directives.unchanged,
63 'blog_background': directives.unchanged,
64 'lid': directives.unchanged,
65 'label': directives.unchanged,
66 }
67 has_content = True
68 add_index = True
69 add_share = True
70 blogpost_class = blogpost_node
71 default_config_bg = "blog_background_page"
73 def suffix_label(self):
74 """
75 returns a suffix to add to a label,
76 it should not be empty for aggregated pages
78 @return str
79 """
80 return ""
82 def run(self):
83 """
84 extracts the information in a dictionary and displays it
85 if the environment is not null
87 @return a list of nodes
88 """
89 # settings
90 sett = self.state.document.settings
91 language_code = sett.language_code
92 if hasattr(sett, "out_blogpostlist"):
93 sett.out_blogpostlist.append(self)
95 # env
96 if hasattr(self.state.document.settings, "env"):
97 env = self.state.document.settings.env
98 else:
99 env = None
101 if env is None:
102 docname = "___unknown_docname___"
103 config = None
104 blog_background = False
105 sharepost = None
106 else:
107 # otherwise, it means sphinx is running
108 docname = env.docname
109 # settings and configuration
110 config = env.config
111 try:
112 blog_background = getattr(
113 config, self.__class__.default_config_bg)
114 except AttributeError as e: # pragma: no cover
115 raise AttributeError("Unable to find '{1}' in \n{0}".format(
116 "\n".join(sorted(config.values)), self.__class__.default_config_bg)) from e
117 sharepost = config.sharepost if self.__class__.add_share else None
119 # post
120 p = {
121 'docname': docname,
122 'lineno': self.lineno,
123 'date': self.options["date"],
124 'title': self.options["title"],
125 'keywords': [a.strip() for a in self.options["keywords"].split(",")],
126 'categories': [a.strip() for a in self.options["categories"].split(",")],
127 'blog_background': self.options.get("blog_background", str(blog_background)).strip() in ("True", "true", "1"),
128 'lid': self.options.get("lid", self.options.get("label", None)),
129 }
131 tag = BlogPost.build_tag(
132 p["date"],
133 p["title"]) if p['lid'] is None else p['lid']
134 targetnode = nodes.target(p['title'], '', ids=[tag])
135 p["target"] = targetnode
136 idbp = tag + "-container"
138 if env is not None:
139 if not hasattr(env, 'blogpost_all'):
140 env.blogpost_all = []
141 env.blogpost_all.append(p)
143 # build node
144 if not docname:
145 raise RuntimeError( # pragma: no cover
146 f'docname is missing in blogpost {docname}.')
147 node = self.__class__.blogpost_class(
148 ids=[idbp], year=p["date"][:4],
149 rawfile=self.options.get("rawfile", None),
150 linktitle=p["title"], lg=language_code,
151 blog_background=p["blog_background"])
152 node.source = docname
153 if not node.source:
154 raise RuntimeError( # pragma: no cover
155 f'node.source is missing in blogpost '
156 f'{self.options.get("rawfile", None)}.')
157 return self.fill_node(node, env, tag, p, language_code,
158 targetnode, sharepost)
160 def fill_node(self, node, env, tag, p, language_code, targetnode, sharepost):
161 """
162 Fills the content of the node.
163 """
164 # add a label
165 suffix_label = self.suffix_label() if not p['lid'] else ""
166 tag = f"{tag}{suffix_label}"
167 tnl = [f".. _{tag}:", ""]
168 title = f"{p['date']} {p['title']}"
169 tnl.append(title)
170 tnl.append("=" * len(title))
171 tnl.append("")
172 if sharepost is not None:
173 tnl.append("")
174 tnl.append(f":sharenet:`{sharepost}`")
175 tnl.append('')
176 tnl.append('')
177 content = StringList(tnl)
178 content = content + self.content
179 try:
180 nested_parse_with_titles(self.state, content, node)
181 except Exception as e: # pragma: no cover
182 from sphinx.util import logging
183 logger = logging.getLogger("blogpost")
184 logger.warning(
185 "[blogpost] unable to parse %r - %s", title, e)
186 raise e
188 # final
189 p['blogpost'] = node
190 self.exe_class = p.copy()
191 p["content"] = content
192 node['classes'] += ["blogpost"]
194 # for the instruction tocdelay.
195 node['toctitle'] = title
196 node['tocid'] = tag
197 node['tocdoc'] = env.docname
199 # end.
200 ns = [node]
201 return ns
204class BlogPostDirectiveAgg(BlogPostDirective):
206 """
207 same but for the same post in a aggregated pages
208 """
209 add_index = False
210 add_share = False
211 blogpost_class = blogpostagg_node
212 default_config_bg = "blog_background"
213 option_spec = {'date': directives.unchanged,
214 'title': directives.unchanged,
215 'keywords': directives.unchanged,
216 'categories': directives.unchanged,
217 'author': directives.unchanged,
218 'rawfile': directives.unchanged,
219 'blog_background': directives.unchanged,
220 }
222 def suffix_label(self):
223 """
224 returns a suffix to add to a label,
225 it should not be empty for aggregated pages
227 @return str
228 """
229 if hasattr(self.state.document.settings, "env"):
230 env = self.state.document.settings.env
231 docname = os.path.split(env.docname)[-1]
232 docname = os.path.splitext(docname)[0]
233 else:
234 env = None
235 docname = ""
236 return "-agg" + docname
238 def fill_node(self, node, env, tag, p, language_code, targetnode, sharepost):
239 """
240 Fill the node of an aggregated page.
241 """
242 # add a label
243 suffix_label = self.suffix_label()
244 container = nodes.container()
245 tnl = [f".. _{tag}{suffix_label}:", ""]
246 content = StringList(tnl)
247 self.state.nested_parse(content, self.content_offset, container)
248 node += container
250 # id section
251 if env is not None:
252 mid = int(env.new_serialno(f"indexblog-u-{p['date'][:4]}")) + 1
253 else:
254 mid = -1
256 # add title
257 sids = f"y{p['date'][:4]}-{mid}"
258 section = nodes.section(ids=[sids])
259 section['year'] = p["date"][:4]
260 section['blogmid'] = mid
261 node += section
262 textnodes, messages = self.state.inline_text(p["title"], self.lineno)
263 section += nodes.title(p["title"], '', *textnodes)
264 section += messages
266 # add date and share buttons
267 tnl = [f":bigger:`::5:{p['date']}`"]
268 if sharepost is not None:
269 tnl.append(f":sharenet:`{sharepost}`")
270 tnl.append('')
271 content = StringList(tnl)
272 content = content + self.content
274 # parse the content into sphinx directive,
275 # it adds it to section
276 container = nodes.container()
277 # nested_parse_with_titles(self.state, content, paragraph)
278 self.state.nested_parse(content, self.content_offset, container)
279 section += container
281 # final
282 p['blogpost'] = node
283 self.exe_class = p.copy()
284 p["content"] = content
285 node['classes'] += ["blogpost"]
287 # target
288 # self.state.add_target(p['title'], '', targetnode, lineno)
290 # index (see site-packages/sphinx/directives/code.py, class Index)
291 if self.__class__.add_index:
292 # it adds an index
293 # self.state.document.note_explicit_target(targetnode)
294 indexnode = addnodes.index()
295 indexnode['entries'] = ne = []
296 indexnode['inline'] = False
297 set_source_info(self, indexnode)
298 for entry in set(p["keywords"] + p["categories"] + [p["date"]]):
299 ne.extend(process_index_entry(entry, tag)) # targetid))
300 ns = [indexnode, targetnode, node]
301 else:
302 ns = [targetnode, node]
304 return ns
307def visit_blogpost_node(self, node):
308 """
309 what to do when visiting a node blogpost
310 the function should have different behaviour,
311 depending on the format, or the setup should
312 specify a different function for each.
313 """
314 if node["blog_background"]:
315 # the node will be in a box
316 self.visit_admonition(node)
319def depart_blogpost_node(self, node):
320 """
321 what to do when leaving a node blogpost
322 the function should have different behaviour,
323 depending on the format, or the setup should
324 specify a different function for each.
325 """
326 if node["blog_background"]:
327 # the node will be in a box
328 self.depart_admonition(node)
331def visit_blogpostagg_node(self, node):
332 """
333 what to do when visiting a node blogpost
334 the function should have different behaviour,
335 depending on the format, or the setup should
336 specify a different function for each.
337 """
338 pass
341def depart_blogpostagg_node(self, node):
342 """
343 what to do when leaving a node blogpost,
344 the function should have different behaviour,
345 depending on the format, or the setup should
346 specify a different function for each.
347 """
348 pass
351def depart_blogpostagg_node_html(self, node):
352 """
353 what to do when leaving a node blogpost,
354 the function should have different behaviour,
355 depending on the format, or the setup should
356 specify a different function for each.
357 """
358 if node.hasattr("year"):
359 rawfile = node["rawfile"]
360 if rawfile is not None:
361 # there is probably better to do
362 # module name is something list doctuils.../[xx].py
363 lg = node["lg"]
364 name = os.path.splitext(os.path.split(rawfile)[-1])[0]
365 name += ".html"
366 year = node["year"]
367 linktitle = node["linktitle"]
368 link = """<p><a class="reference internal" href="{0}/{2}" title="{1}">{3}</a></p>""" \
369 .format(year, linktitle, name, TITLES[lg]["more"])
370 self.body.append(link)
371 else:
372 self.body.append(
373 f"%blogpostagg: link to source only available for HTML: '{type(self)}'\n")
376######################
377# unused, kept as example
378######################
380class blogpostlist_node(nodes.General, nodes.Element):
382 """
383 defines *blogpostlist* node,
384 unused, kept as example
385 """
386 pass
389class BlogPostListDirective(Directive):
391 """
392 unused, kept as example
393 """
395 def run(self):
396 return [BlogPostListDirective.blogpostlist('')]
399def purge_blogpost(app, env, docname):
400 """
401 unused, kept as example
402 """
403 if not hasattr(env, 'blogpost_all'):
404 return
405 env.blogpost_all = [post for post in env.blogpost_all
406 if post['docname'] != docname]
409def process_blogpost_nodes(app, doctree, fromdocname): # pragma: no cover
410 """
411 unused, kept as example
412 """
413 if not app.config.blogpost_include_s:
414 for node in doctree.traverse(blogpost_node):
415 node.parent.remove(node)
417 # Replace all blogpostlist nodes with a list of the collected blogposts.
418 # Augment each blogpost with a backlink to the original location.
419 env = app.builder.env
420 if hasattr(env, "settings") and hasattr(env.settings, "language_code"):
421 lang = env.settings.language_code
422 else:
423 lang = "en"
424 blogmes = TITLES[lang]["blog_entry"]
426 for node in doctree.traverse(blogpostlist_node):
427 if not app.config.blogpost_include_s:
428 node.replace_self([])
429 continue
431 content = []
433 for post_info in env.blogpost_all:
434 para = nodes.paragraph()
435 filename = env.doc2path(post_info['docname'], base=None)
436 description = (_locale(blogmes) % (filename, post_info['lineno']))
437 para += nodes.Text(description, description)
439 # Create a reference
440 newnode = nodes.reference('', '')
441 innernode = nodes.emphasis(_locale('here'), _locale('here'))
442 newnode['refdocname'] = post_info['docname']
443 newnode['refuri'] = app.builder.get_relative_uri(
444 fromdocname, post_info['docname'])
445 try:
446 newnode['refuri'] += '#' + post_info['target']['refid']
447 except Exception as e:
448 raise KeyError("refid in not present in '{0}'".format(
449 post_info['target'])) from e
450 newnode.append(innernode)
451 para += newnode
452 para += nodes.Text('.)', '.)')
454 # Insert into the blogpostlist
455 content.append(post_info['blogpost'])
456 content.append(para)
458 node.replace_self(content)
461def setup(app):
462 """
463 setup for ``blogpost`` (sphinx)
464 """
465 # this command enables the parameter blog_background to be part of the
466 # configuration
467 app.add_config_value('sharepost', None, 'env')
468 app.add_config_value('blog_background', True, 'env')
469 app.add_config_value('blog_background_page', False, 'env')
470 app.add_config_value('out_blogpostlist', [], 'env')
471 if hasattr(app, "add_mapping"):
472 app.add_mapping('blogpost', blogpost_node)
473 app.add_mapping('blogpostagg', blogpostagg_node)
475 # app.add_node(blogpostlist)
476 app.add_node(blogpost_node,
477 html=(visit_blogpost_node, depart_blogpost_node),
478 epub=(visit_blogpost_node, depart_blogpost_node),
479 elatex=(visit_blogpost_node, depart_blogpost_node),
480 latex=(visit_blogpost_node, depart_blogpost_node),
481 rst=(visit_blogpost_node, depart_blogpost_node),
482 md=(visit_blogpost_node, depart_blogpost_node),
483 text=(visit_blogpost_node, depart_blogpost_node))
485 app.add_node(blogpostagg_node,
486 html=(visit_blogpostagg_node, depart_blogpostagg_node_html),
487 epub=(visit_blogpostagg_node, depart_blogpostagg_node_html),
488 elatex=(visit_blogpostagg_node, depart_blogpostagg_node),
489 latex=(visit_blogpostagg_node, depart_blogpostagg_node),
490 rst=(visit_blogpostagg_node, depart_blogpostagg_node),
491 md=(visit_blogpostagg_node, depart_blogpostagg_node),
492 text=(visit_blogpostagg_node, depart_blogpostagg_node))
494 app.add_directive('blogpost', BlogPostDirective)
495 app.add_directive('blogpostagg', BlogPostDirectiveAgg)
496 # app.add_directive('blogpostlist', BlogPostListDirective)
497 # app.connect('doctree-resolved', process_blogpost_nodes)
498 # app.connect('env-purge-doc', purge_blogpost)
499 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}