# -*- coding: utf-8 -*-
"""
Helpers to process blog post included in the documentation.
:githublink:`%|py|6`
"""
import os
import sys
from io import StringIO
from docutils import io as docio
from docutils.core import publish_programmatically
[docs]class BlogPostParseError(Exception):
"""
Exception raised when a error comes after
a blogpost was parsed.
:githublink:`%|py|19`
"""
pass
[docs]class BlogPost:
"""
Defines a blog post.
:githublink:`%|py|27`
"""
[docs] def __init__(self, filename, encoding='utf-8-sig', raise_exception=False, extensions=None):
"""
Creates an instance of a blog post from a file or a string.
:param filename: filename or string
:param encoding: encoding
:param raise_exception: to raise an exception when the blog cannot be parsed
:param extensions: list of extension to use to parse the content of the blog,
if None, it will consider a default list
(see :class:`BlogPost <pyquickhelper.sphinxext.blog_post.BlogPost>` and :func:`get_default_extensions`)
The constructor creates the following members:
* title
* date
* keywords
* categories
* _filename
* _raw
* rst_obj: the object generated by docutils (:class:`BlogPostDirective <pyquickhelper.sphinxext.sphinx_blog_extension.BlogPostDirective>`)
* pub: Publisher
Parameter *raise_exception* catches the standard error.
:githublink:`%|py|52`
"""
if os.path.exists(filename):
with open(filename, "r", encoding=encoding) as f:
try:
content = f.read()
except UnicodeDecodeError as e:
raise Exception(
'unable to read filename (encoding issue):\n File "{0}", line 1'.format(filename)) from e
self._filename = filename
else:
content = filename
self._filename = None
self._raw = content
overrides = {}
overrides["out_blogpostlist"] = []
overrides["blog_background"] = True
overrides["blog_background_page"] = False
overrides["sharepost"] = None
overrides.update({ # 'warning_stream': StringIO(),
'out_blogpostlist': [],
'out_runpythonlist': [],
'master_doc': 'stringblog'
})
if "extensions" not in overrides:
if extensions is None:
# To avoid circular references.
from . import get_default_extensions
# By default, we do not load bokeh extension (slow).
extensions = get_default_extensions(load_bokeh=False)
overrides["extensions"] = extensions
from ..helpgen.sphinxm_mock_app import MockSphinxApp
app = MockSphinxApp.create(confoverrides=overrides)
env = app[0].env
config = env.config
if 'blog_background' not in config:
raise AttributeError("Unable to find 'blog_background' in config:\n{0}".format(
"\n".join(sorted(config.values))))
if 'blog_background_page' not in config:
raise AttributeError("Unable to find 'blog_background_page' in config:\n{0}".format(
"\n".join(sorted(config.values))))
env.temp_data["docname"] = "stringblog"
overrides["env"] = env
config.add('doctitle_xform', True, False, bool)
config.add('initial_header_level', 2, False, int)
config.add('input_encoding', encoding, False, str)
errst = sys.stderr
keeperr = StringIO()
sys.stderr = keeperr
_, pub = publish_programmatically(source_class=docio.StringInput, source=content,
source_path=None, destination_class=docio.StringOutput, destination=None,
destination_path=None, reader=None, reader_name='standalone', parser=None,
parser_name='restructuredtext', writer=None, writer_name='null', settings=None,
settings_spec=None, settings_overrides=overrides, config_section=None,
enable_exit_status=None)
sys.stderr = errst
all_err = keeperr.getvalue()
if len(all_err) > 0:
if raise_exception:
raise BlogPostParseError("unable to parse a blogpost:\n[sphinxerror]-F\n{0}\nFILE\n{1}\nCONTENT\n{2}".format(
all_err, self._filename, content))
# we assume we just need the content, raising a warnings
# might make some process fail later
# warnings.warn("Raw rst was caught but unable to fully parse
# a blogpost:\n[sphinxerror]-H\n{0}\nFILE\n{1}\nCONTENT\n{2}".format(
# all_err, self._filename, content))
# document = pub.writer.document
objects = pub.settings.out_blogpostlist
if len(objects) != 1:
raise BlogPostParseError(
'no blog post (#={1}) in\n File "{0}", line 1'.format(filename, len(objects)))
post = objects[0]
for k in post.options:
setattr(self, k, post.options[k])
self.rst_obj = post
self.pub = pub
self._content = post.content
[docs] def __cmp__(self, other):
"""
This method avoids to get the following error
``TypeError: unorderable types: BlogPost() < BlogPost()``.
:param other: other :class:`BlogPost <pyquickhelper.sphinxext.blog_post.BlogPost>`
:return: -1, 0, or 1
:githublink:`%|py|151`
"""
if self.Date < other.Date:
return -1
elif self.Date > other.Date:
return 1
else:
if self.Tag < other.Tag:
return -1
elif self.Tag > other.Tag:
return 1
else:
raise Exception(
"same tag for two BlogPost: {0}".format(self.Tag))
[docs] def __lt__(self, other):
"""
Tells if this blog should be placed before *other*.
:githublink:`%|py|168`
"""
if self.Date < other.Date:
return True
elif self.Date > other.Date:
return False
else:
if self.Tag < other.Tag:
return True
else:
return False
@property
def Fields(self):
"""
Returns the fields as a dictionary.
:githublink:`%|py|183`
"""
res = dict(title=self.title,
date=self.date,
keywords=self.Keywords,
categories=self.Categories)
if self.BlogBackground is not None:
res["blog_ground"] = self.BlogBackground
if self.Author is not None:
res["author"] = self.Author
return res
@property
def Tag(self):
"""
Produces a tag for the blog post.
:githublink:`%|py|198`
"""
return BlogPost.build_tag(self.Date, self.Title)
[docs] @staticmethod
def build_tag(date, title):
"""
Builds the tag for a post.
:param date: date
:param title: title
:return: tag or label
:githublink:`%|py|209`
"""
return "post-" + date + "-" + \
"".join([c for c in title.lower() if "a" <= c <= "z"])
@property
def FileName(self):
"""
Returns the filename.
:githublink:`%|py|217`
"""
return self._filename
@property
def Title(self):
"""
Returns the title.
:githublink:`%|py|224`
"""
return self.title
@property
def BlogBackground(self):
"""
Returns the blog background or None if not defined.
:githublink:`%|py|231`
"""
return self.blog_ground if hasattr(self, "blog_ground") else None
@property
def Author(self):
"""
Returns the author or None if not defined.
:githublink:`%|py|238`
"""
return self.author if hasattr(self, "author") else None
@property
def Date(self):
"""
Returns the date.
:githublink:`%|py|245`
"""
return self.date
@property
def Year(self):
"""
Returns the year, we assume ``self.date`` is a string like ``YYYY-MM-DD``.
:githublink:`%|py|252`
"""
return self.date[:4]
@property
def Keywords(self):
"""
Returns the keywords.
:githublink:`%|py|259`
"""
return [_.strip() for _ in self.keywords.split(",")]
@property
def Categories(self):
"""
Returns the categories.
:githublink:`%|py|266`
"""
return [_.strip() for _ in self.categories.split(",")]
@property
def Content(self):
"""
Returns the content of the blogpost.
:githublink:`%|py|273`
"""
return self._content
[docs] def post_as_rst(self, language, directive="blogpostagg", cut=False):
"""
Reproduces the text of the blog post,
updates the image links.
:param language: language
:param directive: to specify a different behavior based on
:param cut: truncate the post after the first paragraph
:return: blog post as RST
.. versionadded:: 1.7
Parameter *cut* was added.
:githublink:`%|py|288`
"""
rows = []
rows.append(".. %s::" % directive)
for f, v in self.Fields.items():
if isinstance(v, str):
rows.append(" :%s: %s" % (f, v))
else:
rows.append(" :%s: %s" % (f, ",".join(v)))
if self._filename is not None:
spl = self._filename.replace("\\", "/").split("/")
name = "/".join(spl[-2:])
rows.append(" :rawfile: %s" % name)
rows.append("")
def can_cut(i, r, rows_stack):
rs = r.lstrip()
indent = len(r) - len(rs)
if len(rows_stack) == 0:
if len(rs) > 0:
rows_stack.append(r)
else:
indent2 = len(rows_stack[0]) - len(rows_stack[0].lstrip())
last = rows_stack[-1]
if len(last) > 0:
last = last[-1]
if indent == indent2 and len(rs) == 0 and \
last in {'.', ';', ',', ':', '!', '?'}:
return True
rows_stack.append(r)
return False
rows_stack = []
if directive == "blogpostagg":
for i, r in enumerate(self.Content):
rows.append(" " + self._update_link(r))
if cut and can_cut(i, r, rows_stack):
rows.append("")
rows.append(" ...")
break
else:
for i, r in enumerate(self.Content):
rows.append(" " + r)
if cut and can_cut(i, r, rows_stack):
rows.append("")
rows.append(" ...")
break
rows.append("")
rows.append("")
return "\n".join(rows)
image_tag = ".. image:: "
[docs] def _update_link(self, row):
"""
Changes a link to an image if the page contains one into
*year/img.png*.
:param row: row
:return: new row
:githublink:`%|py|349`
"""
r = row.strip("\r\t ")
if r.startswith(BlogPost.image_tag):
i = len(BlogPost.image_tag)
r2 = row[i:]
if "/" in r2:
return row
row = "{0}{1}/{2}".format(row[:i], self.Year, r2)
return row
else:
return row