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

22 

23 

24class blogpost_node(nodes.Element): 

25 

26 """ 

27 Defines *blogpost* node. 

28 """ 

29 pass 

30 

31 

32class blogpostagg_node(nodes.Element): 

33 

34 """ 

35 Defines *blogpostagg* node. 

36 """ 

37 pass 

38 

39 

40class BlogPostDirective(Directive): 

41 

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: 

46 

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" 

72 

73 def suffix_label(self): 

74 """ 

75 returns a suffix to add to a label, 

76 it should not be empty for aggregated pages 

77 

78 @return str 

79 """ 

80 return "" 

81 

82 def run(self): 

83 """ 

84 extracts the information in a dictionary and displays it 

85 if the environment is not null 

86 

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) 

94 

95 # env 

96 if hasattr(self.state.document.settings, "env"): 

97 env = self.state.document.settings.env 

98 else: 

99 env = None 

100 

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 

118 

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 } 

130 

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" 

136 

137 if env is not None: 

138 if not hasattr(env, 'blogpost_all'): 

139 env.blogpost_all = [] 

140 env.blogpost_all.append(p) 

141 

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

148 

149 return self.fill_node(node, env, tag, p, language_code, targetnode, sharepost) 

150 

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 

178 

179 # final 

180 p['blogpost'] = node 

181 self.exe_class = p.copy() 

182 p["content"] = content 

183 node['classes'] += ["blogpost"] 

184 

185 # for the instruction tocdelay. 

186 node['toctitle'] = title 

187 node['tocid'] = tag 

188 node['tocdoc'] = env.docname 

189 

190 # end. 

191 ns = [node] 

192 return ns 

193 

194 

195class BlogPostDirectiveAgg(BlogPostDirective): 

196 

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 } 

212 

213 def suffix_label(self): 

214 """ 

215 returns a suffix to add to a label, 

216 it should not be empty for aggregated pages 

217 

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 

228 

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 

240 

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 

246 

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 

256 

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 

264 

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 

271 

272 # final 

273 p['blogpost'] = node 

274 self.exe_class = p.copy() 

275 p["content"] = content 

276 node['classes'] += ["blogpost"] 

277 

278 # target 

279 # self.state.add_target(p['title'], '', targetnode, lineno) 

280 

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] 

294 

295 return ns 

296 

297 

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) 

308 

309 

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) 

320 

321 

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 

330 

331 

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 

340 

341 

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

365 

366 

367###################### 

368# unused, kept as example 

369###################### 

370 

371class blogpostlist_node(nodes.General, nodes.Element): 

372 

373 """ 

374 defines *blogpostlist* node, 

375 unused, kept as example 

376 """ 

377 pass 

378 

379 

380class BlogPostListDirective(Directive): 

381 

382 """ 

383 unused, kept as example 

384 """ 

385 

386 def run(self): 

387 return [BlogPostListDirective.blogpostlist('')] 

388 

389 

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] 

398 

399 

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) 

407 

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

416 

417 for node in doctree.traverse(blogpostlist_node): 

418 if not app.config.blogpost_include_s: 

419 node.replace_self([]) 

420 continue 

421 

422 content = [] 

423 

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) 

429 

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('.)', '.)') 

444 

445 # Insert into the blogpostlist 

446 content.append(post_info['blogpost']) 

447 content.append(para) 

448 

449 node.replace_self(content) 

450 

451 

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) 

465 

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

475 

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

484 

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}