Hide keyboard shortcuts

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 

13 

14 

15class BlogPostList: 

16 

17 """ 

18 Defines a list of @see cl BlogPost. 

19 """ 

20 

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``. 

25 

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 

53 

54 def __getitem__(self, key): 

55 """ 

56 usual 

57 """ 

58 return self._blogposts[key] 

59 

60 @staticmethod 

61 def category2url(cat): 

62 """ 

63 Removes accents and spaces to get a clean url. 

64 

65 @param cat category name 

66 @return cleaned category 

67 """ 

68 return remove_diacritics(cat).replace(" ", "_") 

69 

70 @property 

71 def Lang(self): 

72 """ 

73 Returns the language. 

74 """ 

75 return self._language 

76 

77 def __iter__(self): 

78 """ 

79 Iterates on @see cl BlogPost. 

80 """ 

81 for obj in self._blogposts: 

82 yield obj 

83 

84 def __len__(self): 

85 """ 

86 Returns the number of blog posts. 

87 """ 

88 return len(self._blogposts) 

89 

90 def get_categories(self): 

91 """ 

92 Extracts the categories. 

93 

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))) 

100 

101 def get_categories_group(self): 

102 """ 

103 Extracts the categories with the posts associated to it. 

104 

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 

114 

115 def get_keywords(self): 

116 """ 

117 Extracts the categories. 

118 

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))) 

125 

126 def get_months(self): 

127 """ 

128 Extracts the months. 

129 

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)) 

137 

138 def get_months_group(self): 

139 """ 

140 Extracts the months with the posts associated to it. 

141 

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 

151 

152 def get_files(self): 

153 """ 

154 Extracts the files. 

155 

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)) 

162 

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. 

167 

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 

176 

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 

186 

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. 

191 

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 

200 

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 

206 

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). 

215 

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() 

229 

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) 

242 

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)) 

253 

254 # final aggregator 

255 res.extend(self.write_aggregated_chapters(folder)) 

256 

257 return res 

258 

259 def get_image(self, img): 

260 """ 

261 Returns the local path to an image in this folder. 

262 

263 @param img image name (see below) 

264 @return local file 

265 

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) 

278 

279 def write_aggregated_index(self, folder, hidden_files=None, hidden_files_html=None, only_html=True): 

280 """ 

281 Writes an index. 

282 

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)) 

318 

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") 

328 

329 if only_html: 

330 f.write("\n\n") 

331 f.write(".. only:: html\n\n") 

332 

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") 

346 

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") 

352 

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") 

358 

359 img = self.get_image("rss") 

360 shutil.copy(img, folder) 

361 

362 return name 

363 

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. 

368 

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) 

388 

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. 

393 

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 

418 

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. 

423 

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 

446 

447 def write_aggregated_chapters(self, folder): 

448 """ 

449 Writes links to post per categories and per months. 

450 

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)) 

471 

472 res.extend(["", "", ""]) 

473 res.extend([TITLES[self.Lang]["by title:"], "", ""]) 

474 res.extend( 

475 ["", "", ":ref:`{0} <l-mainblog>`".format(TITLES[self.Lang]["allblogs"]), "", ""]) 

476 

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] 

481 

482 ################# 

483 # static methods 

484 ################# 

485 

486 @staticmethod 

487 def divide_list(ld, division): 

488 """ 

489 Divides a list into buckets of *division* items. 

490 

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 

506 

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. 

513 

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 

553 

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. 

560 

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) 

594 

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("") 

607 

608 if index_terms is not None: 

609 rows.append("") 

610 rows.append(".. index:: " + ",".join(index_terms)) 

611 rows.append("") 

612 

613 rows.append("") 

614 rows.append(".. _%s:" % this) 

615 rows.append("") 

616 

617 if bold_title is not None: 

618 rows.append(bold_title) 

619 rows.append("+" * len(bold_title)) 

620 rows.append("") 

621 

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("") 

627 

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) 

636 

637 return "\n".join(rows)