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 <http://sphinx-doc.org/extdev/tutorial.html>`_, 

6`Creating reStructuredText Directives <http://docutils.readthedocs.org/en/sphinx-docs/howto/rst-directives.html>`_ 

7""" 

8import os 

9import sphinx 

10from docutils import nodes 

11from docutils.parsers.rst import Directive 

12from sphinx.locale import _ as _locale 

13from docutils.parsers.rst import directives 

14from docutils.statemachine import StringList 

15from sphinx import addnodes 

16from sphinx.util.nodes import set_source_info, process_index_entry 

17from sphinx.util.nodes import nested_parse_with_titles 

18from .blog_post import BlogPost 

19from ..texthelper.texts_language import TITLES 

20 

21 

22class blogpost_node(nodes.Element): 

23 

24 """ 

25 Defines *blogpost* node. 

26 """ 

27 pass 

28 

29 

30class blogpostagg_node(nodes.Element): 

31 

32 """ 

33 Defines *blogpostagg* node. 

34 """ 

35 pass 

36 

37 

38class BlogPostDirective(Directive): 

39 

40 """ 

41 Extracts information about a blog post described by a directive ``.. blogpost::`` 

42 and modifies the documentation if *env* is not null. The directive handles the following 

43 options: 

44 

45 * *date*: date of the blog (mandatory) 

46 * *title*: title (mandatory) 

47 * *keywords*: keywords, comma separated (mandatory) 

48 * *categories*: categories, comma separated (mandatory) 

49 * *author*: author (optional) 

50 * *blog_background*: can change the blog background (boolean, default is True) 

51 * *lid* or *label*: an id to refer to (optional) 

52 """ 

53 required_arguments = 0 

54 optional_arguments = 0 

55 final_argument_whitespace = True 

56 option_spec = {'date': directives.unchanged, 

57 'title': directives.unchanged, 

58 'keywords': directives.unchanged, 

59 'categories': directives.unchanged, 

60 'author': directives.unchanged, 

61 'blog_background': directives.unchanged, 

62 'lid': directives.unchanged, 

63 'label': directives.unchanged, 

64 } 

65 has_content = True 

66 add_index = True 

67 add_share = True 

68 blogpost_class = blogpost_node 

69 default_config_bg = "blog_background_page" 

70 

71 def suffix_label(self): 

72 """ 

73 returns a suffix to add to a label, 

74 it should not be empty for aggregated pages 

75 

76 @return str 

77 """ 

78 return "" 

79 

80 def run(self): 

81 """ 

82 extracts the information in a dictionary and displays it 

83 if the environment is not null 

84 

85 @return a list of nodes 

86 """ 

87 # settings 

88 sett = self.state.document.settings 

89 language_code = sett.language_code 

90 if hasattr(sett, "out_blogpostlist"): 

91 sett.out_blogpostlist.append(self) 

92 

93 # env 

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

95 env = self.state.document.settings.env 

96 else: 

97 env = None 

98 

99 if env is None: 

100 docname = "___unknown_docname___" 

101 config = None 

102 blog_background = False 

103 sharepost = None 

104 else: 

105 # otherwise, it means sphinx is running 

106 docname = env.docname 

107 # settings and configuration 

108 config = env.config 

109 try: 

110 blog_background = getattr( 

111 config, self.__class__.default_config_bg) 

112 except AttributeError as e: 

113 raise AttributeError("Unable to find '{1}' in \n{0}".format( 

114 "\n".join(sorted(config.values)), self.__class__.default_config_bg)) from e 

115 sharepost = config.sharepost if self.__class__.add_share else None 

116 

117 # post 

118 p = { 

119 'docname': docname, 

120 'lineno': self.lineno, 

121 'date': self.options["date"], 

122 'title': self.options["title"], 

123 'keywords': [a.strip() for a in self.options["keywords"].split(",")], 

124 'categories': [a.strip() for a in self.options["categories"].split(",")], 

125 'blog_background': self.options.get("blog_background", str(blog_background)).strip() in ("True", "true", "1"), 

126 'lid': self.options.get("lid", self.options.get("label", None)), 

127 } 

128 

129 tag = BlogPost.build_tag(p["date"], p["title"]) if p[ 

130 'lid'] is None else p['lid'] 

131 targetnode = nodes.target(p['title'], '', ids=[tag]) 

132 p["target"] = targetnode 

133 idbp = tag + "-container" 

134 

135 if env is not None: 

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

137 env.blogpost_all = [] 

138 env.blogpost_all.append(p) 

139 

140 # build node 

141 node = self.__class__.blogpost_class(ids=[idbp], year=p["date"][:4], 

142 rawfile=self.options.get( 

143 "rawfile", None), 

144 linktitle=p["title"], lg=language_code, 

145 blog_background=p["blog_background"]) 

146 

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

148 

149 def fill_node(self, node, env, tag, p, language_code, targetnode, sharepost): 

150 """ 

151 Fills the content of the node. 

152 """ 

153 # add a label 

154 suffix_label = self.suffix_label() if not p['lid'] else "" 

155 tag = "{0}{1}".format(tag, suffix_label) 

156 tnl = [".. _{0}:".format(tag), ""] 

157 title = "{0} {1}".format(p["date"], p["title"]) 

158 tnl.append(title) 

159 tnl.append("=" * len(title)) 

160 tnl.append("") 

161 if sharepost is not None: 

162 tnl.append("") 

163 tnl.append(":sharenet:`{0}`".format(sharepost)) 

164 tnl.append('') 

165 tnl.append('') 

166 content = StringList(tnl) 

167 content = content + self.content 

168 try: 

169 nested_parse_with_titles(self.state, content, node) 

170 except Exception as e: # pragma: no cover 

171 from sphinx.util import logging 

172 logger = logging.getLogger("blogpost") 

173 logger.warning( 

174 "[blogpost] unable to parse '{0}' - {1}".format(title, e)) 

175 raise e 

176 

177 # final 

178 p['blogpost'] = node 

179 self.exe_class = p.copy() 

180 p["content"] = content 

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

182 

183 # for the instruction tocdelay. 

184 node['toctitle'] = title 

185 node['tocid'] = tag 

186 node['tocdoc'] = env.docname 

187 

188 # end. 

189 ns = [node] 

190 return ns 

191 

192 

193class BlogPostDirectiveAgg(BlogPostDirective): 

194 

195 """ 

196 same but for the same post in a aggregated pages 

197 """ 

198 add_index = False 

199 add_share = False 

200 blogpost_class = blogpostagg_node 

201 default_config_bg = "blog_background" 

202 option_spec = {'date': directives.unchanged, 

203 'title': directives.unchanged, 

204 'keywords': directives.unchanged, 

205 'categories': directives.unchanged, 

206 'author': directives.unchanged, 

207 'rawfile': directives.unchanged, 

208 'blog_background': directives.unchanged, 

209 } 

210 

211 def suffix_label(self): 

212 """ 

213 returns a suffix to add to a label, 

214 it should not be empty for aggregated pages 

215 

216 @return str 

217 """ 

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

219 env = self.state.document.settings.env 

220 docname = os.path.split(env.docname)[-1] 

221 docname = os.path.splitext(docname)[0] 

222 else: 

223 env = None 

224 docname = "" 

225 return "-agg" + docname 

226 

227 def fill_node(self, node, env, tag, p, language_code, targetnode, sharepost): 

228 """ 

229 Fill the node of an aggregated page. 

230 """ 

231 # add a label 

232 suffix_label = self.suffix_label() 

233 container = nodes.container() 

234 tnl = [".. _{0}{1}:".format(tag, suffix_label), ""] 

235 content = StringList(tnl) 

236 self.state.nested_parse(content, self.content_offset, container) 

237 node += container 

238 

239 # id section 

240 if env is not None: 

241 mid = int(env.new_serialno('indexblog-u-%s' % p["date"][:4])) + 1 

242 else: 

243 mid = -1 

244 

245 # add title 

246 sids = "y{0}-{1}".format(p["date"][:4], mid) 

247 section = nodes.section(ids=[sids]) 

248 section['year'] = p["date"][:4] 

249 section['blogmid'] = mid 

250 node += section 

251 textnodes, messages = self.state.inline_text(p["title"], self.lineno) 

252 section += nodes.title(p["title"], '', *textnodes) 

253 section += messages 

254 

255 # add date and share buttons 

256 tnl = [":bigger:`::5:{0}`".format(p["date"])] 

257 if sharepost is not None: 

258 tnl.append(":sharenet:`{0}`".format(sharepost)) 

259 tnl.append('') 

260 content = StringList(tnl) 

261 content = content + self.content 

262 

263 # parse the content into sphinx directive, 

264 # it adds it to section 

265 container = nodes.container() 

266 # nested_parse_with_titles(self.state, content, paragraph) 

267 self.state.nested_parse(content, self.content_offset, container) 

268 section += container 

269 

270 # final 

271 p['blogpost'] = node 

272 self.exe_class = p.copy() 

273 p["content"] = content 

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

275 

276 # target 

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

278 

279 # index (see site-packages/sphinx/directives/code.py, class Index) 

280 if self.__class__.add_index: 

281 # it adds an index 

282 # self.state.document.note_explicit_target(targetnode) 

283 indexnode = addnodes.index() 

284 indexnode['entries'] = ne = [] 

285 indexnode['inline'] = False 

286 set_source_info(self, indexnode) 

287 for entry in set(p["keywords"] + p["categories"] + [p["date"]]): 

288 ne.extend(process_index_entry(entry, tag)) # targetid)) 

289 ns = [indexnode, targetnode, node] 

290 else: 

291 ns = [targetnode, node] 

292 

293 return ns 

294 

295 

296def visit_blogpost_node(self, node): 

297 """ 

298 what to do when visiting a node blogpost 

299 the function should have different behaviour, 

300 depending on the format, or the setup should 

301 specify a different function for each. 

302 """ 

303 if node["blog_background"]: 

304 # the node will be in a box 

305 self.visit_admonition(node) 

306 

307 

308def depart_blogpost_node(self, node): 

309 """ 

310 what to do when leaving a node blogpost 

311 the function should have different behaviour, 

312 depending on the format, or the setup should 

313 specify a different function for each. 

314 """ 

315 if node["blog_background"]: 

316 # the node will be in a box 

317 self.depart_admonition(node) 

318 

319 

320def visit_blogpostagg_node(self, node): 

321 """ 

322 what to do when visiting a node blogpost 

323 the function should have different behaviour, 

324 depending on the format, or the setup should 

325 specify a different function for each. 

326 """ 

327 pass 

328 

329 

330def depart_blogpostagg_node(self, node): 

331 """ 

332 what to do when leaving a node blogpost, 

333 the function should have different behaviour, 

334 depending on the format, or the setup should 

335 specify a different function for each. 

336 """ 

337 pass 

338 

339 

340def depart_blogpostagg_node_html(self, node): 

341 """ 

342 what to do when leaving a node blogpost, 

343 the function should have different behaviour, 

344 depending on the format, or the setup should 

345 specify a different function for each. 

346 """ 

347 if node.hasattr("year"): 

348 rawfile = node["rawfile"] 

349 if rawfile is not None: 

350 # there is probably better to do 

351 # module name is something list doctuils.../[xx].py 

352 lg = node["lg"] 

353 name = os.path.splitext(os.path.split(rawfile)[-1])[0] 

354 name += ".html" 

355 year = node["year"] 

356 linktitle = node["linktitle"] 

357 link = """<p><a class="reference internal" href="{0}/{2}" title="{1}">{3}</a></p>""" \ 

358 .format(year, linktitle, name, TITLES[lg]["more"]) 

359 self.body.append(link) 

360 else: 

361 self.body.append( 

362 "%blogpostagg: link to source only available for HTML: '{}'\n".format(type(self))) 

363 

364 

365###################### 

366# unused, kept as example 

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

368 

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

370 

371 """ 

372 defines *blogpostlist* node, 

373 unused, kept as example 

374 """ 

375 pass 

376 

377 

378class BlogPostListDirective(Directive): 

379 

380 """ 

381 unused, kept as example 

382 """ 

383 

384 def run(self): 

385 return [BlogPostListDirective.blogpostlist('')] 

386 

387 

388def purge_blogpost(app, env, docname): 

389 """ 

390 unused, kept as example 

391 """ 

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

393 return 

394 env.blogpost_all = [post for post in env.blogpost_all 

395 if post['docname'] != docname] 

396 

397 

398def process_blogpost_nodes(app, doctree, fromdocname): # pragma: no cover 

399 """ 

400 unused, kept as example 

401 """ 

402 if not app.config.blogpost_include_s: 

403 for node in doctree.traverse(blogpost_node): 

404 node.parent.remove(node) 

405 

406 # Replace all blogpostlist nodes with a list of the collected blogposts. 

407 # Augment each blogpost with a backlink to the original location. 

408 env = app.builder.env 

409 if hasattr(env, "settings") and hasattr(env.settings, "language_code"): 

410 lang = env.settings.language_code 

411 else: 

412 lang = "en" 

413 blogmes = TITLES[lang]["blog_entry"] 

414 

415 for node in doctree.traverse(blogpostlist_node): 

416 if not app.config.blogpost_include_s: 

417 node.replace_self([]) 

418 continue 

419 

420 content = [] 

421 

422 for post_info in env.blogpost_all: 

423 para = nodes.paragraph() 

424 filename = env.doc2path(post_info['docname'], base=None) 

425 description = (_locale(blogmes) % (filename, post_info['lineno'])) 

426 para += nodes.Text(description, description) 

427 

428 # Create a reference 

429 newnode = nodes.reference('', '') 

430 innernode = nodes.emphasis(_locale('here'), _locale('here')) 

431 newnode['refdocname'] = post_info['docname'] 

432 newnode['refuri'] = app.builder.get_relative_uri( 

433 fromdocname, post_info['docname']) 

434 try: 

435 newnode['refuri'] += '#' + post_info['target']['refid'] 

436 except Exception as e: 

437 raise KeyError("refid in not present in '{0}'".format( 

438 post_info['target'])) from e 

439 newnode.append(innernode) 

440 para += newnode 

441 para += nodes.Text('.)', '.)') 

442 

443 # Insert into the blogpostlist 

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

445 content.append(para) 

446 

447 node.replace_self(content) 

448 

449 

450def setup(app): 

451 """ 

452 setup for ``blogpost`` (sphinx) 

453 """ 

454 # this command enables the parameter blog_background to be part of the 

455 # configuration 

456 app.add_config_value('sharepost', None, 'env') 

457 app.add_config_value('blog_background', True, 'env') 

458 app.add_config_value('blog_background_page', False, 'env') 

459 app.add_config_value('out_blogpostlist', [], 'env') 

460 if hasattr(app, "add_mapping"): 

461 app.add_mapping('blogpost', blogpost_node) 

462 app.add_mapping('blogpostagg', blogpostagg_node) 

463 

464 # app.add_node(blogpostlist) 

465 app.add_node(blogpost_node, 

466 html=(visit_blogpost_node, depart_blogpost_node), 

467 epub=(visit_blogpost_node, depart_blogpost_node), 

468 elatex=(visit_blogpost_node, depart_blogpost_node), 

469 latex=(visit_blogpost_node, depart_blogpost_node), 

470 rst=(visit_blogpost_node, depart_blogpost_node), 

471 md=(visit_blogpost_node, depart_blogpost_node), 

472 text=(visit_blogpost_node, depart_blogpost_node)) 

473 

474 app.add_node(blogpostagg_node, 

475 html=(visit_blogpostagg_node, depart_blogpostagg_node_html), 

476 epub=(visit_blogpostagg_node, depart_blogpostagg_node_html), 

477 elatex=(visit_blogpostagg_node, depart_blogpostagg_node), 

478 latex=(visit_blogpostagg_node, depart_blogpostagg_node), 

479 rst=(visit_blogpostagg_node, depart_blogpostagg_node), 

480 md=(visit_blogpostagg_node, depart_blogpostagg_node), 

481 text=(visit_blogpostagg_node, depart_blogpostagg_node)) 

482 

483 app.add_directive('blogpost', BlogPostDirective) 

484 app.add_directive('blogpostagg', BlogPostDirectiveAgg) 

485 #app.add_directive('blogpostlist', BlogPostListDirective) 

486 #app.connect('doctree-resolved', process_blogpost_nodes) 

487 #app.connect('env-purge-doc', purge_blogpost) 

488 return {'version': sphinx.__display_version__, 'parallel_read_safe': True}