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 build :epkg:`RST` extra files inserted in the documentation.
5"""
6import os
7import shutil
8from .blog_post import BlogPost
9from .build_rss import build_rss
10from ..texthelper.texts_language import TITLES
11from ..texthelper.diacritic_helper import remove_diacritics
12from ..loghelper import noLOG
15class BlogPostList:
17 """
18 Defines a list of @see cl BlogPost.
19 """
21 def __init__(self, folder, encoding="utf8", language="en", extensions=None, fLOG=noLOG):
22 """
23 Creates a list of @see cl BlogPost, we assume each blog
24 post belongs to a subfolder ``YYYY``.
26 @param folder folder when to find files
27 @param encoding encoding
28 @param language language
29 @param extensions list of extension to use to parse the content of the blog,
30 if None, it will consider a default list
31 (@see cl BlogPost and @see fn get_default_extensions)
32 @param fLOG logging function
33 """
34 self._blogposts = []
35 sub = os.listdir(folder)
36 for s in sorted(sub):
37 full = os.path.join(folder, s)
38 if os.path.isdir(full):
39 fLOG("[BlogPostList] reading folder %r" % full)
40 posts = os.listdir(full)
41 for post in sorted(posts):
42 if os.path.splitext(post)[-1] in [".rst"]:
43 fpost = os.path.join(full, post)
44 fLOG(" reading post %r" % post)
45 obj = BlogPost(fpost, encoding=encoding,
46 extensions=extensions)
47 self._blogposts.append((obj.date, obj))
48 fLOG("[BlogPostList] end reading folder %r" % full)
49 self._blogposts.sort(reverse=True)
50 self._blogposts = [_[1] for _ in self._blogposts]
51 self._encoding = encoding
52 self._language = language
54 def __getitem__(self, key):
55 """
56 usual
57 """
58 return self._blogposts[key]
60 @staticmethod
61 def category2url(cat):
62 """
63 Removes accents and spaces to get a clean url.
65 @param cat category name
66 @return cleaned category
67 """
68 return remove_diacritics(cat).replace(" ", "_")
70 @property
71 def Lang(self):
72 """
73 Returns the language.
74 """
75 return self._language
77 def __iter__(self):
78 """
79 Iterates on @see cl BlogPost.
80 """
81 for obj in self._blogposts:
82 yield obj
84 def __len__(self):
85 """
86 Returns the number of blog posts.
87 """
88 return len(self._blogposts)
90 def get_categories(self):
91 """
92 Extracts the categories.
94 @return list of sorted categories
95 """
96 cats = []
97 for post in self:
98 cats.extend(post.Categories)
99 return list(sorted(set(cats)))
101 def get_categories_group(self):
102 """
103 Extracts the categories with the posts associated to it.
105 @return dictionary (category, list of posts)
106 """
107 m = {}
108 for post in self:
109 for cat in post.Categories:
110 if cat not in m:
111 m[cat] = []
112 m[cat].append(post)
113 return m
115 def get_keywords(self):
116 """
117 Extracts the categories.
119 @return list of sorted keywords
120 """
121 keys = []
122 for post in self:
123 keys.extend(post.Keywords)
124 return list(sorted(set(keys)))
126 def get_months(self):
127 """
128 Extracts the months.
130 @return list of sorted months (more recent first)
131 """
132 m = []
133 for post in self:
134 d = "-".join(post.Date.split("-")[:2])
135 m.append(d)
136 return list(sorted(set(m), reverse=True))
138 def get_months_group(self):
139 """
140 Extracts the months with the posts associated to it.
142 @return dictionary (months, list of posts)
143 """
144 m = {}
145 for post in self:
146 d = "-".join(post.Date.split("-")[:2])
147 if d not in m:
148 m[d] = []
149 m[d].append(post)
150 return m
152 def get_files(self):
153 """
154 Extracts the files.
156 @return list of sorted months (more recent first)
157 """
158 m = []
159 for post in self:
160 m.append(post.FileName)
161 return list(sorted(set(m), reverse=True))
163 def get_rst_links_up(self):
164 """
165 Builds the :epkg:`rst` links to months or categories to displays
166 at the beginning of the aggregated pages.
168 @return list of rst_links
169 """
170 ens = self.get_categories_group()
171 if len(ens) > 5:
172 sorted_end = list(sorted((v, m) for m, v in ens.items()))
173 ens = {}
174 for v, m in sorted_end[-5:]:
175 ens[m] = v
177 links = []
178 for m, v in sorted(ens.items()):
179 if len(v) <= 2:
180 # we skip categories with less than 2 blog post
181 continue
182 link = ":ref:`{0} ({1}) <ap-cat-{0}-0>`".format(
183 BlogPostList.category2url(m), len(v))
184 links.append(link)
185 return links
187 def get_rst_links_down(self):
188 """
189 Builds the :epkg:`rst` links to months or categories to displays
190 the bottom of the aggregated pages.
192 @return list of rst_links
193 """
194 ens = self.get_months_group()
195 if len(ens) > 5:
196 sorted_end = list(sorted((m, v) for m, v in ens.items()))
197 ens = {}
198 for m, v in sorted_end[-5:]:
199 ens[m] = v
201 links = []
202 for m, v in sorted(ens.items()):
203 link = ":ref:`{0} ({1}) <ap-month-{0}-0>`".format(m, len(v))
204 links.append(link)
205 return links
207 def write_aggregated(self, folder, division=10,
208 blog_title="__BLOG_TITLE__",
209 blog_description="__BLOG_DESCRIPTION__",
210 blog_root="__BLOG_ROOT__",
211 only_html_index=True,
212 only_html_agg=False):
213 """
214 Writes posts in a aggregated manner (post, categories, months).
216 @param folder where to write them
217 @param division add a new page every *division* items
218 @param blog_title blog title
219 @param blog_description blog description
220 @param blog_root blog root (publish url)
221 @param only_html_index add item ``.. only:: html`` and indent everything
222 after the main index
223 @param only_html_agg add item ``.. only:: html`` and indent everything
224 for aggregated pages
225 @return list of produced files
226 """
227 link_up = self.get_rst_links_up()
228 link_down = self.get_rst_links_down()
230 # rss
231 rss = os.path.join(folder, "rss.xml")
232 keep = []
233 for _ in self:
234 if len(keep) >= 10:
235 break
236 keep.append(_)
237 c = build_rss(keep, blog_title=blog_title,
238 blog_description=blog_description,
239 blog_root=blog_root)
240 with open(rss, "w", encoding=self._encoding) as f:
241 f.write(c)
243 # aggregated pages
244 res = []
245 res.extend(self.write_aggregated_posts(folder, division, rst_links_up=link_up,
246 rst_links_down=link_down, only_html=only_html_agg))
247 res.extend(self.write_aggregated_categories(folder, division, rst_links_up=link_up,
248 rst_links_down=link_down, only_html=only_html_agg))
249 res.extend(self.write_aggregated_months(folder, division, rst_links_up=link_up,
250 rst_links_down=link_down, only_html=only_html_agg))
251 res.append(self.write_aggregated_index(
252 folder, hidden_files=None, hidden_files_html=res, only_html=only_html_index))
254 # final aggregator
255 res.extend(self.write_aggregated_chapters(folder))
257 return res
259 def get_image(self, img):
260 """
261 Returns the local path to an image in this folder.
263 @param img image name (see below)
264 @return local file
266 Allowed image names:
267 - rss: image for RSS stream
268 """
269 if img == "rss":
270 img = "feed-icon-16x16.png"
271 loc = os.path.abspath(os.path.dirname(__file__))
272 img = os.path.join(loc, img)
273 if not os.path.exists(img):
274 raise FileNotFoundError("unable to find: " + img)
275 return img
276 else:
277 raise FileNotFoundError("unable to get image name: " + img)
279 def write_aggregated_index(self, folder, hidden_files=None, hidden_files_html=None, only_html=True):
280 """
281 Writes an index.
283 @param folder where to write the file
284 @param hidden_files creates an hidden *toctree* and a @see cl tocdelay_node.
285 @param only_html add item ``.. only:: html`` and indent everything
286 after the main index
287 @param hidden_files_html add item ``.. only:: html`` for these pages
288 @return filename
289 """
290 indent = " " if only_html else ""
291 name = os.path.join(folder, "blogindex.rst")
292 with open(name, "w", encoding=self._encoding) as f:
293 f.write("\n")
294 f.write(":orphan:\n")
295 f.write("\n")
296 f.write(".. _l-mainblog:\n")
297 f.write("\n")
298 f.write("\n")
299 f.write("Blog Gallery\n")
300 f.write("============\n")
301 f.write("\n")
302 f.write(".. tocdelay::\n")
303 f.write("\n")
304 for item in self:
305 name_file = os.path.splitext(
306 os.path.split(item.FileName)[-1])[0]
307 f.write(" {0} - {1} <{2}/{3}>\n".format(
308 item.Date, item.Title, item.Date[:4], name_file))
309 f.write("\n\n")
310 f.write(".. toctree::\n")
311 f.write(" :hidden:\n")
312 f.write("\n")
313 for item_id, item in enumerate(self):
314 fl = os.path.split(item.FileName)[-1]
315 fl = os.path.splitext(fl)[0]
316 f.write(
317 " blog {2} <{0}/{1}>\n".format(item.Date[:4], fl, item_id))
319 if hidden_files is not None:
320 f.write("\n\n")
321 f.write(".. toctree::\n")
322 f.write(" :hidden:\n")
323 f.write("\n")
324 for hid, h in enumerate(hidden_files):
325 f.write(" blog {1} <{0}>\n".format(
326 os.path.splitext(os.path.split(h)[-1])[0], hid))
327 f.write("\n\n")
329 if only_html:
330 f.write("\n\n")
331 f.write(".. only:: html\n\n")
333 if hidden_files_html is not None:
334 f.write(indent + ".. toctree::\n")
335 f.write(indent + " :hidden:\n")
336 f.write("\n")
337 for item_id, item in enumerate(self):
338 fl = os.path.split(item.FileName)[-1]
339 fl = os.path.splitext(fl)[0]
340 f.write(
341 indent + " {2} <{0}/{1}>\n".format(item.Date[:4], fl, item_id))
342 for hid, h in enumerate(hidden_files_html):
343 f.write(
344 indent + " blog {1} <{0}>\n".format(os.path.splitext(os.path.split(h)[-1])[0], hid))
345 f.write("\n\n")
347 f.write("\n")
348 f.write(indent + ".. image:: feed-icon-16x16.png\n\n")
349 f.write(
350 indent + ":download:`{0} rss <rss.xml>`\n".format(TITLES[self.Lang]["download"]))
351 f.write("\n\n\n")
353 f.write(
354 indent + ":ref:`{0} <hblog-blog>`, ".format(TITLES[self.Lang]["main"]))
355 f.write(
356 indent + ":ref:`{0} <ap-main-0>`".format(TITLES[self.Lang]["main2"]))
357 f.write("\n\n\n")
359 img = self.get_image("rss")
360 shutil.copy(img, folder)
362 return name
364 def write_aggregated_posts(self, folder, division=10, rst_links_up=None,
365 rst_links_down=None, only_html=True):
366 """
367 Writes posts in a aggregated manner.
369 @param folder where to write them
370 @param division add a new page every *division* items
371 @param rst_links_up list of rst_links to add at the beginning of a page
372 @param rst_links_down list of rst_links to add at the bottom of a page
373 @param only_html add item ``.. only:: html`` and indent everything
374 @return list of produced files
375 """
376 return BlogPostList.write_aggregated_post_list(folder=folder,
377 lp=list(
378 _ for _ in self),
379 division=division,
380 prefix="main",
381 encoding=self._encoding,
382 rst_links_up=rst_links_up,
383 rst_links_down=rst_links_down,
384 index_terms=["blog"],
385 language=self.Lang,
386 bold_title=TITLES[self.Lang]["main_title"],
387 only_html=only_html)
389 def write_aggregated_categories(self, folder, division=10, rst_links_up=None,
390 rst_links_down=None, only_html=True):
391 """
392 Writes posts in a aggregated manner per categories.
394 @param folder where to write them
395 @param division add a new page every *division* items
396 @param rst_links_up list of rst_links to add at the beginning of a page
397 @param rst_links_down list of rst_links to add at the bottom of a page
398 @param only_html add item ``.. only:: html`` and indent everything
399 @return list of produced files
400 """
401 cats = self.get_categories()
402 res = []
403 for cat in cats:
404 posts = [_ for _ in self if cat in _.Categories]
405 url_cat = BlogPostList.category2url(cat)
406 add = BlogPostList.write_aggregated_post_list(folder=folder,
407 lp=posts,
408 division=division,
409 prefix="cat-" + url_cat,
410 encoding=self._encoding,
411 rst_links_up=rst_links_up,
412 rst_links_down=rst_links_down,
413 index_terms=[cat],
414 bold_title=cat,
415 only_html=only_html)
416 res.extend(add)
417 return res
419 def write_aggregated_months(self, folder, division=10, rst_links_up=None,
420 rst_links_down=None, only_html=True):
421 """
422 Writes posts in a aggregated manner per months.
424 @param folder where to write them
425 @param division add a new page every *division* items
426 @param rst_links_up list of rst_links to add at the beginning of a page
427 @param rst_links_down list of rst_links to add at the bottom of a page
428 @param only_html add item ``.. only:: html`` and indent everything
429 @return list of produced files
430 """
431 mo = self.get_months()
432 res = []
433 for m in mo:
434 posts = [_ for _ in self if _.Date.startswith(m)]
435 add = BlogPostList.write_aggregated_post_list(folder=folder,
436 lp=posts,
437 division=division,
438 prefix="month-" + m,
439 encoding=self._encoding,
440 rst_links_up=rst_links_up, rst_links_down=rst_links_down,
441 index_terms=[m],
442 bold_title=m,
443 only_html=only_html)
444 res.extend(add)
445 return res
447 def write_aggregated_chapters(self, folder):
448 """
449 Writes links to post per categories and per months.
451 @param folder where to write them
452 @return list of produced files
453 """
454 cats = sorted([(k, len(v))
455 for k, v in self.get_categories_group().items()])
456 months = sorted(
457 [(k, len(v)) for k, v in self.get_months_group().items()], reverse=True)
458 res = ["", ":orphan:", "", ".. _hblog-blog:",
459 "", "", "Blog", "====", "", ""]
460 res.extend(
461 ["* :ref:`{0} <ap-main-0>`".format(TITLES[self.Lang]["page1"]), "", ""])
462 res.extend([TITLES[self.Lang]["by category:"], "", ""])
463 for cat, nb in cats:
464 res.append(
465 "* :ref:`{0} ({1}) <ap-cat-{0}-0>`".format(BlogPostList.category2url(cat), nb))
466 res.extend(["", "", ""])
467 res.extend([TITLES[self.Lang]["by month:"], "", ""])
468 res.extend(["", "", ""])
469 for mon, nb in months:
470 res.append("* :ref:`{0} ({1}) <ap-month-{0}-0>`".format(mon, nb))
472 res.extend(["", "", ""])
473 res.extend([TITLES[self.Lang]["by title:"], "", ""])
474 res.extend(
475 ["", "", ":ref:`{0} <l-mainblog>`".format(TITLES[self.Lang]["allblogs"]), "", ""])
477 filename = os.path.join(folder, "index_blog.rst")
478 with open(filename, "w", encoding="utf8") as f:
479 f.write("\n".join(res))
480 return [filename]
482 #################
483 # static methods
484 #################
486 @staticmethod
487 def divide_list(ld, division):
488 """
489 Divides a list into buckets of *division* items.
491 @param ld list of to divide
492 @param division bucket size
493 @return list fo buckets
494 """
495 buckets = []
496 current = []
497 for obj in ld:
498 if len(current) < division:
499 current.append(obj)
500 else:
501 buckets.append(current)
502 current = [obj]
503 if len(current) > 0:
504 buckets.append(current)
505 return buckets
507 @staticmethod
508 def write_aggregated_post_list(folder, lp, division, prefix, encoding,
509 rst_links_up=None, rst_links_down=None, index_terms=None,
510 bold_title=None, language="en", only_html=True):
511 """
512 Writes list of posts in an aggregated manners.
514 @param folder when to write the aggregated posts
515 @param lp list of posts
516 @param division bucket size
517 @param prefix prefix name for the files
518 @param encoding encoding for the written files
519 @param rst_links_up list of rst_links to add at the beginning of a page
520 @param rst_links_down list of rst_links to add at the bottom of a page
521 @param index_terms terms to index on the first bucket
522 @param bold_title title to display at the beginning of the page
523 @param language language
524 @param only_html add item ``.. only:: html`` and indent everything
525 @return list of produced files
526 """
527 res = []
528 buckets = BlogPostList.divide_list(lp, division)
529 for i, b in enumerate(buckets):
530 if bold_title is not None:
531 title = "{0} - {1}/{2}".format(bold_title, i + 1, len(buckets))
532 else:
533 title = None
534 name = os.path.join(folder, "%s_%04d.rst" % (prefix, i))
535 prev = "ap-%s-%d" % (prefix, i - 1) if i > 0 else None
536 this = "ap-%s-%d" % (prefix, i)
537 next = "ap-%s-%d" % (prefix, i + 1) \
538 if i < len(buckets) - 1 else None
539 content = BlogPostList.produce_aggregated_post_page(
540 name, b, this, prev, next,
541 rst_links_up=rst_links_up,
542 rst_links_down=rst_links_down,
543 index_terms=index_terms if i == 0 else None,
544 bold_title=title, language=language)
545 if only_html:
546 lines = content.split("\n")
547 head = "\n:orphan:\n\n.. only:: html\n\n"
548 content = head + "\n".join(" " + _ for _ in lines)
549 with open(name, "w", encoding=encoding) as f:
550 f.write(content)
551 res.append(name)
552 return res
554 @staticmethod
555 def produce_aggregated_post_page(name, lp, this, prev, next, main_page="Blog",
556 rst_links_up=None, rst_links_down=None,
557 index_terms=None, bold_title=None, language="en"):
558 """
559 Writes the content of an aggregate page of blog posts.
561 @param name filename to write
562 @param lp list of posts
563 @param this reference to this page
564 @param prev reference to the previous page
565 @param next reference to the next page
566 @param main_page name of the main page
567 @param rst_links_up list of rst_links to add at the beginning of a page
568 @param rst_links_down list of rst_links to add at the bottom of a page
569 @param index_terms terms to index
570 @param bold_title title to display of the beginning of the page
571 @param language language
572 @return content of the page
573 """
574 direction = "|rss_image| "
575 if prev is not None:
576 direction += ":ref:`<== <%s>` " % prev
577 if bold_title is not None:
578 if len(direction) > 0:
579 direction += " "
580 direction += "**{0}**".format(bold_title)
581 if next is not None:
582 if len(direction) > 0:
583 direction += " "
584 direction += ":ref:`==> <%s>`" % next
585 arrows = direction
586 if main_page is not None:
587 if len(direction) > 0:
588 direction += " "
589 direction += ":ref:`%s <ap-main-0>`" % main_page
590 if rst_links_up is not None:
591 if len(direction) > 0:
592 direction += " "
593 direction += " ".join(rst_links_up)
595 rows = []
596 rows.append("")
597 rows.append(":orphan:")
598 rows.append("")
599 rows.append(direction)
600 rows.append("")
601 rows.append(".. |rss_image| image:: feed-icon-16x16.png")
602 rows.append(" :target: ../_downloads/rss.xml")
603 rows.append(" :alt: RSS")
604 rows.append("")
605 rows.append("----")
606 rows.append("")
608 if index_terms is not None:
609 rows.append("")
610 rows.append(".. index:: " + ",".join(index_terms))
611 rows.append("")
613 rows.append("")
614 rows.append(".. _%s:" % this)
615 rows.append("")
617 if bold_title is not None:
618 rows.append(bold_title)
619 rows.append("+" * len(bold_title))
620 rows.append("")
622 for post in lp:
623 text = post.post_as_rst(language=language, cut=True)
624 rows.append(text)
625 rows.append("")
626 rows.append("")
628 rows.append("")
629 rows.append("----")
630 rows.append("")
631 if rst_links_down is not None:
632 if len(arrows) > 0:
633 arrows += " "
634 arrows += " ".join(rst_links_down)
635 rows.append(arrows)
637 return "\n".join(rows)