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 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:
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(p["date"], p["title"]) if p[
132 'lid'] is None else p['lid']
133 targetnode = nodes.target(p['title'], '', ids=[tag])
134 p["target"] = targetnode
135 idbp = tag + "-container"
137 if env is not None:
138 if not hasattr(env, 'blogpost_all'):
139 env.blogpost_all = []
140 env.blogpost_all.append(p)
142 # build node
143 node = self.__class__.blogpost_class(ids=[idbp], year=p["date"][:4],
144 rawfile=self.options.get(
145 "rawfile", None),
146 linktitle=p["title"], lg=language_code,
147 blog_background=p["blog_background"])
149 return self.fill_node(node, env, tag, p, language_code, targetnode, sharepost)
151 def fill_node(self, node, env, tag, p, language_code, targetnode, sharepost):
152 """
153 Fills the content of the node.
154 """
155 # add a label
156 suffix_label = self.suffix_label() if not p['lid'] else ""
157 tag = "{0}{1}".format(tag, suffix_label)
158 tnl = [".. _{0}:".format(tag), ""]
159 title = "{0} {1}".format(p["date"], p["title"])
160 tnl.append(title)
161 tnl.append("=" * len(title))
162 tnl.append("")
163 if sharepost is not None:
164 tnl.append("")
165 tnl.append(":sharenet:`{0}`".format(sharepost))
166 tnl.append('')
167 tnl.append('')
168 content = StringList(tnl)
169 content = content + self.content
170 try:
171 nested_parse_with_titles(self.state, content, node)
172 except Exception as e: # pragma: no cover
173 from sphinx.util import logging
174 logger = logging.getLogger("blogpost")
175 logger.warning(
176 "[blogpost] unable to parse '{0}' - {1}".format(title, e))
177 raise e
179 # final
180 p['blogpost'] = node
181 self.exe_class = p.copy()
182 p["content"] = content
183 node['classes'] += ["blogpost"]
185 # for the instruction tocdelay.
186 node['toctitle'] = title
187 node['tocid'] = tag
188 node['tocdoc'] = env.docname
190 # end.
191 ns = [node]
192 return ns
195class BlogPostDirectiveAgg(BlogPostDirective):
197 """
198 same but for the same post in a aggregated pages
199 """
200 add_index = False
201 add_share = False
202 blogpost_class = blogpostagg_node
203 default_config_bg = "blog_background"
204 option_spec = {'date': directives.unchanged,
205 'title': directives.unchanged,
206 'keywords': directives.unchanged,
207 'categories': directives.unchanged,
208 'author': directives.unchanged,
209 'rawfile': directives.unchanged,
210 'blog_background': directives.unchanged,
211 }
213 def suffix_label(self):
214 """
215 returns a suffix to add to a label,
216 it should not be empty for aggregated pages
218 @return str
219 """
220 if hasattr(self.state.document.settings, "env"):
221 env = self.state.document.settings.env
222 docname = os.path.split(env.docname)[-1]
223 docname = os.path.splitext(docname)[0]
224 else:
225 env = None
226 docname = ""
227 return "-agg" + docname
229 def fill_node(self, node, env, tag, p, language_code, targetnode, sharepost):
230 """
231 Fill the node of an aggregated page.
232 """
233 # add a label
234 suffix_label = self.suffix_label()
235 container = nodes.container()
236 tnl = [".. _{0}{1}:".format(tag, suffix_label), ""]
237 content = StringList(tnl)
238 self.state.nested_parse(content, self.content_offset, container)
239 node += container
241 # id section
242 if env is not None:
243 mid = int(env.new_serialno('indexblog-u-%s' % p["date"][:4])) + 1
244 else:
245 mid = -1
247 # add title
248 sids = "y{0}-{1}".format(p["date"][:4], mid)
249 section = nodes.section(ids=[sids])
250 section['year'] = p["date"][:4]
251 section['blogmid'] = mid
252 node += section
253 textnodes, messages = self.state.inline_text(p["title"], self.lineno)
254 section += nodes.title(p["title"], '', *textnodes)
255 section += messages
257 # add date and share buttons
258 tnl = [":bigger:`::5:{0}`".format(p["date"])]
259 if sharepost is not None:
260 tnl.append(":sharenet:`{0}`".format(sharepost))
261 tnl.append('')
262 content = StringList(tnl)
263 content = content + self.content
265 # parse the content into sphinx directive,
266 # it adds it to section
267 container = nodes.container()
268 # nested_parse_with_titles(self.state, content, paragraph)
269 self.state.nested_parse(content, self.content_offset, container)
270 section += container
272 # final
273 p['blogpost'] = node
274 self.exe_class = p.copy()
275 p["content"] = content
276 node['classes'] += ["blogpost"]
278 # target
279 # self.state.add_target(p['title'], '', targetnode, lineno)
281 # index (see site-packages/sphinx/directives/code.py, class Index)
282 if self.__class__.add_index:
283 # it adds an index
284 # self.state.document.note_explicit_target(targetnode)
285 indexnode = addnodes.index()
286 indexnode['entries'] = ne = []
287 indexnode['inline'] = False
288 set_source_info(self, indexnode)
289 for entry in set(p["keywords"] + p["categories"] + [p["date"]]):
290 ne.extend(process_index_entry(entry, tag)) # targetid))
291 ns = [indexnode, targetnode, node]
292 else:
293 ns = [targetnode, node]
295 return ns
298def visit_blogpost_node(self, node):
299 """
300 what to do when visiting a node blogpost
301 the function should have different behaviour,
302 depending on the format, or the setup should
303 specify a different function for each.
304 """
305 if node["blog_background"]:
306 # the node will be in a box
307 self.visit_admonition(node)
310def depart_blogpost_node(self, node):
311 """
312 what to do when leaving a node blogpost
313 the function should have different behaviour,
314 depending on the format, or the setup should
315 specify a different function for each.
316 """
317 if node["blog_background"]:
318 # the node will be in a box
319 self.depart_admonition(node)
322def visit_blogpostagg_node(self, node):
323 """
324 what to do when visiting a node blogpost
325 the function should have different behaviour,
326 depending on the format, or the setup should
327 specify a different function for each.
328 """
329 pass
332def depart_blogpostagg_node(self, node):
333 """
334 what to do when leaving a node blogpost,
335 the function should have different behaviour,
336 depending on the format, or the setup should
337 specify a different function for each.
338 """
339 pass
342def depart_blogpostagg_node_html(self, node):
343 """
344 what to do when leaving a node blogpost,
345 the function should have different behaviour,
346 depending on the format, or the setup should
347 specify a different function for each.
348 """
349 if node.hasattr("year"):
350 rawfile = node["rawfile"]
351 if rawfile is not None:
352 # there is probably better to do
353 # module name is something list doctuils.../[xx].py
354 lg = node["lg"]
355 name = os.path.splitext(os.path.split(rawfile)[-1])[0]
356 name += ".html"
357 year = node["year"]
358 linktitle = node["linktitle"]
359 link = """<p><a class="reference internal" href="{0}/{2}" title="{1}">{3}</a></p>""" \
360 .format(year, linktitle, name, TITLES[lg]["more"])
361 self.body.append(link)
362 else:
363 self.body.append(
364 "%blogpostagg: link to source only available for HTML: '{}'\n".format(type(self)))
367######################
368# unused, kept as example
369######################
371class blogpostlist_node(nodes.General, nodes.Element):
373 """
374 defines *blogpostlist* node,
375 unused, kept as example
376 """
377 pass
380class BlogPostListDirective(Directive):
382 """
383 unused, kept as example
384 """
386 def run(self):
387 return [BlogPostListDirective.blogpostlist('')]
390def purge_blogpost(app, env, docname):
391 """
392 unused, kept as example
393 """
394 if not hasattr(env, 'blogpost_all'):
395 return
396 env.blogpost_all = [post for post in env.blogpost_all
397 if post['docname'] != docname]
400def process_blogpost_nodes(app, doctree, fromdocname): # pragma: no cover
401 """
402 unused, kept as example
403 """
404 if not app.config.blogpost_include_s:
405 for node in doctree.traverse(blogpost_node):
406 node.parent.remove(node)
408 # Replace all blogpostlist nodes with a list of the collected blogposts.
409 # Augment each blogpost with a backlink to the original location.
410 env = app.builder.env
411 if hasattr(env, "settings") and hasattr(env.settings, "language_code"):
412 lang = env.settings.language_code
413 else:
414 lang = "en"
415 blogmes = TITLES[lang]["blog_entry"]
417 for node in doctree.traverse(blogpostlist_node):
418 if not app.config.blogpost_include_s:
419 node.replace_self([])
420 continue
422 content = []
424 for post_info in env.blogpost_all:
425 para = nodes.paragraph()
426 filename = env.doc2path(post_info['docname'], base=None)
427 description = (_locale(blogmes) % (filename, post_info['lineno']))
428 para += nodes.Text(description, description)
430 # Create a reference
431 newnode = nodes.reference('', '')
432 innernode = nodes.emphasis(_locale('here'), _locale('here'))
433 newnode['refdocname'] = post_info['docname']
434 newnode['refuri'] = app.builder.get_relative_uri(
435 fromdocname, post_info['docname'])
436 try:
437 newnode['refuri'] += '#' + post_info['target']['refid']
438 except Exception as e:
439 raise KeyError("refid in not present in '{0}'".format(
440 post_info['target'])) from e
441 newnode.append(innernode)
442 para += newnode
443 para += nodes.Text('.)', '.)')
445 # Insert into the blogpostlist
446 content.append(post_info['blogpost'])
447 content.append(para)
449 node.replace_self(content)
452def setup(app):
453 """
454 setup for ``blogpost`` (sphinx)
455 """
456 # this command enables the parameter blog_background to be part of the
457 # configuration
458 app.add_config_value('sharepost', None, 'env')
459 app.add_config_value('blog_background', True, 'env')
460 app.add_config_value('blog_background_page', False, 'env')
461 app.add_config_value('out_blogpostlist', [], 'env')
462 if hasattr(app, "add_mapping"):
463 app.add_mapping('blogpost', blogpost_node)
464 app.add_mapping('blogpostagg', blogpostagg_node)
466 # app.add_node(blogpostlist)
467 app.add_node(blogpost_node,
468 html=(visit_blogpost_node, depart_blogpost_node),
469 epub=(visit_blogpost_node, depart_blogpost_node),
470 elatex=(visit_blogpost_node, depart_blogpost_node),
471 latex=(visit_blogpost_node, depart_blogpost_node),
472 rst=(visit_blogpost_node, depart_blogpost_node),
473 md=(visit_blogpost_node, depart_blogpost_node),
474 text=(visit_blogpost_node, depart_blogpost_node))
476 app.add_node(blogpostagg_node,
477 html=(visit_blogpostagg_node, depart_blogpostagg_node_html),
478 epub=(visit_blogpostagg_node, depart_blogpostagg_node_html),
479 elatex=(visit_blogpostagg_node, depart_blogpostagg_node),
480 latex=(visit_blogpostagg_node, depart_blogpostagg_node),
481 rst=(visit_blogpostagg_node, depart_blogpostagg_node),
482 md=(visit_blogpostagg_node, depart_blogpostagg_node),
483 text=(visit_blogpostagg_node, depart_blogpostagg_node))
485 app.add_directive('blogpost', BlogPostDirective)
486 app.add_directive('blogpostagg', BlogPostDirectiveAgg)
487 #app.add_directive('blogpostlist', BlogPostListDirective)
488 #app.connect('doctree-resolved', process_blogpost_nodes)
489 #app.connect('env-purge-doc', purge_blogpost)
490 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}