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 process blog post included in the documentation. 

5""" 

6import os 

7from io import StringIO 

8from contextlib import redirect_stdout, redirect_stderr 

9from docutils import io as docio 

10from docutils.core import publish_programmatically 

11 

12 

13class BlogPostParseError(Exception): 

14 

15 """ 

16 Exception raised when a error comes after 

17 a blogpost was parsed. 

18 """ 

19 pass 

20 

21 

22class BlogPost: 

23 

24 """ 

25 Defines a blog post. 

26 """ 

27 

28 def __init__(self, filename, encoding='utf-8-sig', raise_exception=False, extensions=None): 

29 """ 

30 Creates an instance of a blog post from a file or a string. 

31 

32 @param filename filename or string 

33 @param encoding encoding 

34 @param raise_exception to raise an exception when the blog cannot be parsed 

35 @param extensions list of extension to use to parse the content of the blog, 

36 if None, it will consider a default list 

37 (see @see cl BlogPost and @see fn get_default_extensions) 

38 

39 The constructor creates the following members: 

40 

41 * title 

42 * date 

43 * keywords 

44 * categories 

45 * _filename 

46 * _raw 

47 * rst_obj: the object generated by docutils (@see cl BlogPostDirective) 

48 * pub: Publisher 

49 

50 Parameter *raise_exception* catches the standard error. 

51 Option `:process:` of command `.. runpython::` should be 

52 used within a blog post to avoid having the same process use 

53 sphinx at the same time. 

54 """ 

55 if os.path.exists(filename): 

56 with open(filename, "r", encoding=encoding) as f: 

57 try: 

58 content = f.read() 

59 except UnicodeDecodeError as e: 

60 raise Exception( 

61 'unable to read filename (encoding issue):\n File "{0}", line 1'.format(filename)) from e 

62 self._filename = filename 

63 else: 

64 content = filename 

65 self._filename = None 

66 

67 self._raw = content 

68 

69 overrides = {} 

70 overrides["out_blogpostlist"] = [] 

71 overrides["blog_background"] = True 

72 overrides["blog_background_page"] = False 

73 overrides["sharepost"] = None 

74 

75 overrides.update({ # 'warning_stream': StringIO(), 

76 'out_blogpostlist': [], 

77 'out_runpythonlist': [], 

78 'master_doc': 'stringblog' 

79 }) 

80 

81 if "extensions" not in overrides: 

82 if extensions is None: 

83 # To avoid circular references. 

84 from . import get_default_extensions 

85 # By default, we do not load bokeh extension (slow). 

86 extensions = get_default_extensions(load_bokeh=False) 

87 overrides["extensions"] = extensions 

88 

89 from ..helpgen.sphinxm_mock_app import MockSphinxApp 

90 app = MockSphinxApp.create(confoverrides=overrides) 

91 env = app[0].env 

92 config = env.config 

93 

94 if 'blog_background' not in config: 

95 raise AttributeError( 

96 "Unable to find 'blog_background' in config:\n{0}".format( 

97 "\n".join(sorted(config.values)))) 

98 if 'blog_background_page' not in config: 

99 raise AttributeError( 

100 "Unable to find 'blog_background_page' in config:\n{0}".format( 

101 "\n".join(sorted(config.values)))) 

102 if 'epkg_dictionary' in config: 

103 if len(config.epkg_dictionary) > 0: 

104 overrides['epkg_dictionary'] = config.epkg_dictionary 

105 else: 

106 from ..helpgen.default_conf import get_epkg_dictionary 

107 overrides['epkg_dictionary'] = get_epkg_dictionary() 

108 

109 env.temp_data["docname"] = "stringblog" 

110 overrides["env"] = env 

111 

112 config.add('doctitle_xform', True, False, bool) 

113 config.add('initial_header_level', 2, False, int) 

114 config.add('input_encoding', encoding, False, str) 

115 

116 keepout = StringIO() 

117 keeperr = StringIO() 

118 with redirect_stdout(keepout): 

119 with redirect_stderr(keeperr): 

120 _, pub = publish_programmatically( 

121 source_class=docio.StringInput, source=content, 

122 source_path=None, destination_class=docio.StringOutput, destination=None, 

123 destination_path=None, reader=None, reader_name='standalone', parser=None, 

124 parser_name='restructuredtext', writer=None, writer_name='null', settings=None, 

125 settings_spec=None, settings_overrides=overrides, config_section=None, 

126 enable_exit_status=None) 

127 

128 all_err = keeperr.getvalue() 

129 if len(all_err) > 0: 

130 lines = all_err.strip(' \n\r').split('\n') 

131 lines = [_ for _ in lines 

132 if ("in epkg_dictionary" not in _ and 

133 "to be local relative or absolute" not in _)] 

134 std = keepout.getvalue().strip('\n\r\t ') 

135 if len(lines) > 0 and raise_exception: 

136 raise BlogPostParseError( 

137 "Unable to parse a blogpost:\n[sphinxerror]-F\n{0}" 

138 "\nFILE\n{1}\nCONTENT\n{2}\n--OUT--\n{3}".format( 

139 all_err, self._filename, content, keepout.getvalue())) 

140 if len(lines) > 0: 

141 print(all_err) 

142 if len(std) > 3: 

143 print(std) 

144 else: 

145 for _ in all_err.strip(' \n\r').split('\n'): 

146 print(" ", _) 

147 if len(std) > 3: 

148 print(std) 

149 # we assume we just need the content, raising a warnings 

150 # might make some process fail later 

151 # warnings.warn("Raw rst was caught but unable to fully parse 

152 # a blogpost:\n[sphinxerror]-H\n{0}\nFILE\n{1}\nCONTENT\n{2}".format( 

153 # all_err, self._filename, content)) 

154 

155 # document = pub.writer.document 

156 objects = pub.settings.out_blogpostlist 

157 

158 if len(objects) != 1: 

159 raise BlogPostParseError( 

160 'no blog post (#={1}) in\n File "{0}", line 1'.format(filename, len(objects))) 

161 

162 post = objects[0] 

163 for k in post.options: 

164 setattr(self, k, post.options[k]) 

165 self.rst_obj = post 

166 self.pub = pub 

167 self._content = post.content 

168 

169 def __cmp__(self, other): 

170 """ 

171 This method avoids to get the following error 

172 ``TypeError: unorderable types: BlogPost() < BlogPost()``. 

173 

174 @param other other @see cl BlogPost 

175 @return -1, 0, or 1 

176 """ 

177 if self.Date < other.Date: 

178 return -1 

179 elif self.Date > other.Date: 

180 return 1 

181 else: 

182 if self.Tag < other.Tag: 

183 return -1 

184 elif self.Tag > other.Tag: 

185 return 1 

186 else: 

187 raise Exception( 

188 "same tag for two BlogPost: {0}".format(self.Tag)) 

189 

190 def __lt__(self, other): 

191 """ 

192 Tells if this blog should be placed before *other*. 

193 """ 

194 if self.Date < other.Date: 

195 return True 

196 elif self.Date > other.Date: 

197 return False 

198 else: 

199 if self.Tag < other.Tag: 

200 return True 

201 else: 

202 return False 

203 

204 @property 

205 def Fields(self): 

206 """ 

207 Returns the fields as a dictionary. 

208 """ 

209 res = dict(title=self.title, 

210 date=self.date, 

211 keywords=self.Keywords, 

212 categories=self.Categories) 

213 if self.BlogBackground is not None: 

214 res["blog_ground"] = self.BlogBackground 

215 if self.Author is not None: 

216 res["author"] = self.Author 

217 return res 

218 

219 @property 

220 def Tag(self): 

221 """ 

222 Produces a tag for the blog post. 

223 """ 

224 return BlogPost.build_tag(self.Date, self.Title) 

225 

226 @staticmethod 

227 def build_tag(date, title): 

228 """ 

229 Builds the tag for a post. 

230 

231 @param date date 

232 @param title title 

233 @return tag or label 

234 """ 

235 return "post-" + date + "-" + \ 

236 "".join([c for c in title.lower() if "a" <= c <= "z"]) 

237 

238 @property 

239 def FileName(self): 

240 """ 

241 Returns the filename. 

242 """ 

243 return self._filename 

244 

245 @property 

246 def Title(self): 

247 """ 

248 Returns the title. 

249 """ 

250 return self.title 

251 

252 @property 

253 def BlogBackground(self): 

254 """ 

255 Returns the blog background or None if not defined. 

256 """ 

257 return self.blog_ground if hasattr(self, "blog_ground") else None 

258 

259 @property 

260 def Author(self): 

261 """ 

262 Returns the author or None if not defined. 

263 """ 

264 return self.author if hasattr(self, "author") else None 

265 

266 @property 

267 def Date(self): 

268 """ 

269 Returns the date. 

270 """ 

271 return self.date 

272 

273 @property 

274 def Year(self): 

275 """ 

276 Returns the year, we assume ``self.date`` is a string like ``YYYY-MM-DD``. 

277 """ 

278 return self.date[:4] 

279 

280 @property 

281 def Keywords(self): 

282 """ 

283 Returns the keywords. 

284 """ 

285 return [_.strip() for _ in self.keywords.split(",")] 

286 

287 @property 

288 def Categories(self): 

289 """ 

290 Returns the categories. 

291 """ 

292 return [_.strip() for _ in self.categories.split(",")] 

293 

294 @property 

295 def Content(self): 

296 """ 

297 Returns the content of the blogpost. 

298 """ 

299 return self._content 

300 

301 def post_as_rst(self, language, directive="blogpostagg", cut=False): 

302 """ 

303 Reproduces the text of the blog post, 

304 updates the image links. 

305 

306 @param language language 

307 @param directive to specify a different behavior based on 

308 @param cut truncate the post after the first paragraph 

309 @return blog post as RST 

310 """ 

311 rows = [] 

312 rows.append(".. %s::" % directive) 

313 for f, v in self.Fields.items(): 

314 if isinstance(v, str): 

315 rows.append(" :%s: %s" % (f, v)) 

316 else: 

317 rows.append(" :%s: %s" % (f, ",".join(v))) 

318 if self._filename is not None: 

319 spl = self._filename.replace("\\", "/").split("/") 

320 name = "/".join(spl[-2:]) 

321 rows.append(" :rawfile: %s" % name) 

322 rows.append("") 

323 

324 def can_cut(i, r, rows_stack): 

325 rs = r.lstrip() 

326 indent = len(r) - len(rs) 

327 if len(rows_stack) == 0: 

328 if len(rs) > 0: 

329 rows_stack.append(r) 

330 else: 

331 indent2 = len(rows_stack[0]) - len(rows_stack[0].lstrip()) 

332 last = rows_stack[-1] 

333 if len(last) > 0: 

334 last = last[-1] 

335 if indent == indent2 and len(rs) == 0 and \ 

336 last in {'.', ';', ',', ':', '!', '?'}: 

337 return True 

338 rows_stack.append(r) 

339 return False 

340 

341 rows_stack = [] 

342 if directive == "blogpostagg": 

343 for i, r in enumerate(self.Content): 

344 rows.append(" " + self._update_link(r)) 

345 if cut and can_cut(i, r, rows_stack): 

346 rows.append("") 

347 rows.append(" ...") 

348 break 

349 else: 

350 for i, r in enumerate(self.Content): 

351 rows.append(" " + r) 

352 if cut and can_cut(i, r, rows_stack): 

353 rows.append("") 

354 rows.append(" ...") 

355 break 

356 

357 rows.append("") 

358 rows.append("") 

359 

360 return "\n".join(rows) 

361 

362 image_tag = ".. image:: " 

363 

364 def _update_link(self, row): 

365 """ 

366 Changes a link to an image if the page contains one into 

367 *year/img.png*. 

368 

369 @param row row 

370 @return new row 

371 """ 

372 r = row.strip("\r\t ") 

373 if r.startswith(BlogPost.image_tag): 

374 i = len(BlogPost.image_tag) 

375 r2 = row[i:] 

376 if "/" in r2: 

377 return row 

378 row = "{0}{1}/{2}".format(row[:i], self.Year, r2) 

379 return row 

380 else: 

381 return row