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 Helpers to process blog post included in the documentation.
5"""
6import os
7from io import StringIO
8from contextlib import redirect_stdout, redirect_stderr
9from docutils import io as docio
10from docutils.core import publish_programmatically
13class BlogPostParseError(Exception):
15 """
16 Exception raised when a error comes after
17 a blogpost was parsed.
18 """
19 pass
22class BlogPost:
24 """
25 Defines a blog post.
26 """
28 def __init__(self, filename, encoding='utf-8-sig', raise_exception=False, extensions=None):
29 """
30 Creates an instance of a blog post from a file or a string.
32 @param filename filename or string
33 @param encoding encoding
34 @param raise_exception to raise an exception when the blog cannot be parsed
35 @param extensions list of extension to use to parse the content of the blog,
36 if None, it will consider a default list
37 (see @see cl BlogPost and @see fn get_default_extensions)
39 The constructor creates the following members:
41 * title
42 * date
43 * keywords
44 * categories
45 * _filename
46 * _raw
47 * rst_obj: the object generated by docutils (@see cl BlogPostDirective)
48 * pub: Publisher
50 Parameter *raise_exception* catches the standard error.
51 Option `:process:` of command `.. runpython::` should be
52 used within a blog post to avoid having the same process use
53 sphinx at the same time.
54 """
55 if os.path.exists(filename):
56 with open(filename, "r", encoding=encoding) as f:
57 try:
58 content = f.read()
59 except UnicodeDecodeError as e:
60 raise Exception(
61 'unable to read filename (encoding issue):\n File "{0}", line 1'.format(filename)) from e
62 self._filename = filename
63 else:
64 content = filename
65 self._filename = None
67 self._raw = content
69 overrides = {}
70 overrides["out_blogpostlist"] = []
71 overrides["blog_background"] = True
72 overrides["blog_background_page"] = False
73 overrides["sharepost"] = None
75 overrides.update({ # 'warning_stream': StringIO(),
76 'out_blogpostlist': [],
77 'out_runpythonlist': [],
78 'master_doc': 'stringblog'
79 })
81 if "extensions" not in overrides:
82 if extensions is None:
83 # To avoid circular references.
84 from . import get_default_extensions
85 # By default, we do not load bokeh extension (slow).
86 extensions = get_default_extensions(load_bokeh=False)
87 overrides["extensions"] = extensions
89 from ..helpgen.sphinxm_mock_app import MockSphinxApp
90 app = MockSphinxApp.create(confoverrides=overrides)
91 env = app[0].env
92 config = env.config
94 if 'blog_background' not in config:
95 raise AttributeError(
96 "Unable to find 'blog_background' in config:\n{0}".format(
97 "\n".join(sorted(config.values))))
98 if 'blog_background_page' not in config:
99 raise AttributeError(
100 "Unable to find 'blog_background_page' in config:\n{0}".format(
101 "\n".join(sorted(config.values))))
102 if 'epkg_dictionary' in config:
103 if len(config.epkg_dictionary) > 0:
104 overrides['epkg_dictionary'] = config.epkg_dictionary
105 else:
106 from ..helpgen.default_conf import get_epkg_dictionary
107 overrides['epkg_dictionary'] = get_epkg_dictionary()
109 env.temp_data["docname"] = "stringblog"
110 overrides["env"] = env
112 config.add('doctitle_xform', True, False, bool)
113 config.add('initial_header_level', 2, False, int)
114 config.add('input_encoding', encoding, False, str)
116 keepout = StringIO()
117 keeperr = StringIO()
118 with redirect_stdout(keepout):
119 with redirect_stderr(keeperr):
120 _, pub = publish_programmatically(
121 source_class=docio.StringInput, source=content,
122 source_path=None, destination_class=docio.StringOutput, destination=None,
123 destination_path=None, reader=None, reader_name='standalone', parser=None,
124 parser_name='restructuredtext', writer=None, writer_name='null', settings=None,
125 settings_spec=None, settings_overrides=overrides, config_section=None,
126 enable_exit_status=None)
128 all_err = keeperr.getvalue()
129 if len(all_err) > 0:
130 lines = all_err.strip(' \n\r').split('\n')
131 lines = [_ for _ in lines
132 if ("in epkg_dictionary" not in _ and
133 "to be local relative or absolute" not in _)]
134 std = keepout.getvalue().strip('\n\r\t ')
135 if len(lines) > 0 and raise_exception:
136 raise BlogPostParseError(
137 "Unable to parse a blogpost:\n[sphinxerror]-F\n{0}"
138 "\nFILE\n{1}\nCONTENT\n{2}\n--OUT--\n{3}".format(
139 all_err, self._filename, content, keepout.getvalue()))
140 if len(lines) > 0:
141 print(all_err)
142 if len(std) > 3:
143 print(std)
144 else:
145 for _ in all_err.strip(' \n\r').split('\n'):
146 print(" ", _)
147 if len(std) > 3:
148 print(std)
149 # we assume we just need the content, raising a warnings
150 # might make some process fail later
151 # warnings.warn("Raw rst was caught but unable to fully parse
152 # a blogpost:\n[sphinxerror]-H\n{0}\nFILE\n{1}\nCONTENT\n{2}".format(
153 # all_err, self._filename, content))
155 # document = pub.writer.document
156 objects = pub.settings.out_blogpostlist
158 if len(objects) != 1:
159 raise BlogPostParseError(
160 'no blog post (#={1}) in\n File "{0}", line 1'.format(filename, len(objects)))
162 post = objects[0]
163 for k in post.options:
164 setattr(self, k, post.options[k])
165 self.rst_obj = post
166 self.pub = pub
167 self._content = post.content
169 def __cmp__(self, other):
170 """
171 This method avoids to get the following error
172 ``TypeError: unorderable types: BlogPost() < BlogPost()``.
174 @param other other @see cl BlogPost
175 @return -1, 0, or 1
176 """
177 if self.Date < other.Date:
178 return -1
179 elif self.Date > other.Date:
180 return 1
181 else:
182 if self.Tag < other.Tag:
183 return -1
184 elif self.Tag > other.Tag:
185 return 1
186 else:
187 raise Exception(
188 "same tag for two BlogPost: {0}".format(self.Tag))
190 def __lt__(self, other):
191 """
192 Tells if this blog should be placed before *other*.
193 """
194 if self.Date < other.Date:
195 return True
196 elif self.Date > other.Date:
197 return False
198 else:
199 if self.Tag < other.Tag:
200 return True
201 else:
202 return False
204 @property
205 def Fields(self):
206 """
207 Returns the fields as a dictionary.
208 """
209 res = dict(title=self.title,
210 date=self.date,
211 keywords=self.Keywords,
212 categories=self.Categories)
213 if self.BlogBackground is not None:
214 res["blog_ground"] = self.BlogBackground
215 if self.Author is not None:
216 res["author"] = self.Author
217 return res
219 @property
220 def Tag(self):
221 """
222 Produces a tag for the blog post.
223 """
224 return BlogPost.build_tag(self.Date, self.Title)
226 @staticmethod
227 def build_tag(date, title):
228 """
229 Builds the tag for a post.
231 @param date date
232 @param title title
233 @return tag or label
234 """
235 return "post-" + date + "-" + \
236 "".join([c for c in title.lower() if "a" <= c <= "z"])
238 @property
239 def FileName(self):
240 """
241 Returns the filename.
242 """
243 return self._filename
245 @property
246 def Title(self):
247 """
248 Returns the title.
249 """
250 return self.title
252 @property
253 def BlogBackground(self):
254 """
255 Returns the blog background or None if not defined.
256 """
257 return self.blog_ground if hasattr(self, "blog_ground") else None
259 @property
260 def Author(self):
261 """
262 Returns the author or None if not defined.
263 """
264 return self.author if hasattr(self, "author") else None
266 @property
267 def Date(self):
268 """
269 Returns the date.
270 """
271 return self.date
273 @property
274 def Year(self):
275 """
276 Returns the year, we assume ``self.date`` is a string like ``YYYY-MM-DD``.
277 """
278 return self.date[:4]
280 @property
281 def Keywords(self):
282 """
283 Returns the keywords.
284 """
285 return [_.strip() for _ in self.keywords.split(",")]
287 @property
288 def Categories(self):
289 """
290 Returns the categories.
291 """
292 return [_.strip() for _ in self.categories.split(",")]
294 @property
295 def Content(self):
296 """
297 Returns the content of the blogpost.
298 """
299 return self._content
301 def post_as_rst(self, language, directive="blogpostagg", cut=False):
302 """
303 Reproduces the text of the blog post,
304 updates the image links.
306 @param language language
307 @param directive to specify a different behavior based on
308 @param cut truncate the post after the first paragraph
309 @return blog post as RST
310 """
311 rows = []
312 rows.append(".. %s::" % directive)
313 for f, v in self.Fields.items():
314 if isinstance(v, str):
315 rows.append(" :%s: %s" % (f, v))
316 else:
317 rows.append(" :%s: %s" % (f, ",".join(v)))
318 if self._filename is not None:
319 spl = self._filename.replace("\\", "/").split("/")
320 name = "/".join(spl[-2:])
321 rows.append(" :rawfile: %s" % name)
322 rows.append("")
324 def can_cut(i, r, rows_stack):
325 rs = r.lstrip()
326 indent = len(r) - len(rs)
327 if len(rows_stack) == 0:
328 if len(rs) > 0:
329 rows_stack.append(r)
330 else:
331 indent2 = len(rows_stack[0]) - len(rows_stack[0].lstrip())
332 last = rows_stack[-1]
333 if len(last) > 0:
334 last = last[-1]
335 if indent == indent2 and len(rs) == 0 and \
336 last in {'.', ';', ',', ':', '!', '?'}:
337 return True
338 rows_stack.append(r)
339 return False
341 rows_stack = []
342 if directive == "blogpostagg":
343 for i, r in enumerate(self.Content):
344 rows.append(" " + self._update_link(r))
345 if cut and can_cut(i, r, rows_stack):
346 rows.append("")
347 rows.append(" ...")
348 break
349 else:
350 for i, r in enumerate(self.Content):
351 rows.append(" " + r)
352 if cut and can_cut(i, r, rows_stack):
353 rows.append("")
354 rows.append(" ...")
355 break
357 rows.append("")
358 rows.append("")
360 return "\n".join(rows)
362 image_tag = ".. image:: "
364 def _update_link(self, row):
365 """
366 Changes a link to an image if the page contains one into
367 *year/img.png*.
369 @param row row
370 @return new row
371 """
372 r = row.strip("\r\t ")
373 if r.startswith(BlogPost.image_tag):
374 i = len(BlogPost.image_tag)
375 r2 = row[i:]
376 if "/" in r2:
377 return row
378 row = "{0}{1}/{2}".format(row[:i], self.Year, r2)
379 return row
380 else:
381 return row