Coverage for C:\Repos\leo-editor\leo\core\leoRst.py: 44%

456 statements  

« prev     ^ index     » next       coverage.py v6.4, created at 2022-05-24 10:21 -0500

1# -*- coding: utf-8 -*- 

2#@+leo-ver=5-thin 

3#@+node:ekr.20090502071837.3: * @file leoRst.py 

4#@@first 

5#@+<< docstring >> 

6#@+node:ekr.20090502071837.4: ** << docstring >> 

7"""Support for restructured text (rST), adapted from rst3 plugin. 

8 

9For full documentation, see: http://leoeditor.com/rstplugin3.html 

10 

11To generate documents from rST files, Python's docutils_ module must be 

12installed. The code will use the SilverCity_ syntax coloring package if is is 

13available.""" 

14#@-<< docstring >> 

15#@+<< imports >> 

16#@+node:ekr.20100908120927.5971: ** << imports >> (leoRst) 

17import io 

18import os 

19import re 

20import time 

21# Third-part imports... 

22try: 

23 import docutils 

24 import docutils.core 

25 from docutils import parsers 

26 from docutils.parsers import rst 

27except Exception: 

28 docutils = None # type:ignore 

29# Leo imports. 

30from leo.core import leoGlobals as g 

31# Aliases & traces. 

32StringIO = io.StringIO 

33if 'plugins' in getattr(g.app, 'debug', []): 

34 print('leoRst.py: docutils:', bool(docutils)) 

35 print('leoRst.py: parsers:', bool(parsers)) 

36 print('leoRst.py: rst:', bool(rst)) 

37#@-<< imports >> 

38#@+others 

39#@+node:ekr.20150509035745.1: ** cmd (decorator) 

40def cmd(name): 

41 """Command decorator for the RstCommands class.""" 

42 return g.new_cmd_decorator(name, ['c', 'rstCommands',]) 

43#@+node:ekr.20090502071837.33: ** class RstCommands 

44class RstCommands: 

45 """ 

46 A class to convert @rst nodes to rST markup. 

47 """ 

48 #@+others 

49 #@+node:ekr.20090502071837.34: *3* rst: Birth 

50 #@+node:ekr.20090502071837.35: *4* rst.__init__ 

51 def __init__(self, c): 

52 """Ctor for the RstCommand class.""" 

53 self.c = c 

54 # 

55 # Statistics. 

56 self.n_intermediate = 0 # Number of intermediate files written. 

57 self.n_docutils = 0 # Number of docutils files written. 

58 # 

59 # Http support for HtmlParserClass. See http_addNodeMarker. 

60 self.anchor_map = {} # Keys are anchors. Values are positions 

61 self.http_map = {} # Keys are named hyperlink targets. Value are positions. 

62 self.nodeNumber = 0 # Unique node number. 

63 # 

64 # For writing. 

65 self.at_auto_underlines = '' # Full set of underlining characters. 

66 self.at_auto_write = False # True: in @auto-rst importer. 

67 self.encoding = 'utf-8' # From any @encoding directive. 

68 self.path = '' # The path from any @path directive. 

69 self.result_list = [] # The intermediate results. 

70 self.root = None # The @rst node being processed. 

71 # 

72 # Default settings. 

73 self.default_underline_characters = '#=+*^~`-:><-' 

74 self.user_filter_b = None 

75 self.user_filter_h = None 

76 # 

77 # Complete the init. 

78 self.reloadSettings() 

79 #@+node:ekr.20210326084034.1: *4* rst.reloadSettings 

80 def reloadSettings(self): 

81 """RstCommand.reloadSettings""" 

82 c = self.c 

83 getBool, getString = c.config.getBool, c.config.getString 

84 # 

85 # Reporting options. 

86 self.silent = not getBool('rst3-verbose', default=True) 

87 # 

88 # Http options. 

89 self.http_server_support = getBool('rst3-http-server-support', default=False) 

90 self.node_begin_marker = getString('rst3-node-begin-marker') or 'http-node-marker-' 

91 # 

92 # Output options. 

93 self.default_path = getString('rst3-default-path') or '' 

94 self.generate_rst_header_comment = getBool('rst3-generate-rst-header-comment', default=True) 

95 self.underline_characters = ( 

96 getString('rst3-underline-characters') 

97 or self.default_underline_characters) 

98 self.write_intermediate_file = getBool('rst3-write-intermediate-file', default=True) 

99 self.write_intermediate_extension = getString('rst3-write-intermediate-extension') or '.txt' 

100 # 

101 # Docutils options. 

102 self.call_docutils = getBool('rst3-call-docutils', default=True) 

103 self.publish_argv_for_missing_stylesheets = getString('rst3-publish-argv-for-missing-stylesheets') or '' 

104 self.stylesheet_embed = getBool('rst3-stylesheet-embed', default=False) # New in leoSettings.leo. 

105 self.stylesheet_name = getString('rst3-stylesheet-name') or 'default.css' 

106 self.stylesheet_path = getString('rst3-stylesheet-path') or '' 

107 #@+node:ekr.20100813041139.5920: *3* rst: Entry points 

108 #@+node:ekr.20210403150303.1: *4* rst.rst-convert-legacy-outline 

109 @cmd('rst-convert-legacy-outline') 

110 @cmd('convert-legacy-rst-outline') 

111 def convert_legacy_outline(self, event=None): 

112 """ 

113 Convert @rst-preformat nodes and `@ @rst-options` doc parts. 

114 """ 

115 c = self.c 

116 for p in c.all_unique_positions(): 

117 if g.match_word(p.h, 0, '@rst-preformat'): 

118 self.preformat(p) 

119 self.convert_rst_options(p) 

120 #@+node:ekr.20210403153112.1: *5* rst.convert_rst_options 

121 options_pat = re.compile(r'^@ @rst-options', re.MULTILINE) 

122 default_pat = re.compile(r'^default_path\s*=(.*)$', re.MULTILINE) 

123 

124 def convert_rst_options(self, p): 

125 """ 

126 Convert options @doc parts. Change headline to @path <fn>. 

127 """ 

128 m1 = self.options_pat.search(p.b) 

129 m2 = self.default_pat.search(p.b) 

130 if m1 and m2 and m2.start() > m1.start(): 

131 fn = m2.group(1).strip() 

132 if fn: 

133 old_h = p.h 

134 p.h = f"@path {fn}" 

135 print(f"{old_h} => {p.h}") 

136 #@+node:ekr.20210403151958.1: *5* rst.preformat 

137 def preformat(self, p): 

138 """Convert p.b as if preformatted. Change headline to @rst-no-head""" 

139 if not p.b.strip(): 

140 return 

141 p.b = '::\n\n' + ''.join( 

142 f" {s}" if s.strip() else '\n' 

143 for s in g.splitLines(p.b)) 

144 old_h = p.h 

145 p.h = '@rst-no-head' 

146 print(f"{old_h} => {p.h}") 

147 #@+node:ekr.20090511055302.5793: *4* rst.rst3 command & helpers 

148 @cmd('rst3') 

149 def rst3(self, event=None): 

150 """Write all @rst nodes.""" 

151 t1 = time.time() 

152 self.n_intermediate = self.n_docutils = 0 

153 self.processTopTree(self.c.p) 

154 t2 = time.time() 

155 g.es_print( 

156 f"rst3: wrote...\n" 

157 f"{self.n_intermediate:4} intermediate file{g.plural(self.n_intermediate)}\n" 

158 f"{self.n_docutils:4} docutils file{g.plural(self.n_docutils)}\n" 

159 f"in {t2 - t1:4.2f} sec.") 

160 #@+node:ekr.20090502071837.62: *5* rst.processTopTree 

161 def processTopTree(self, p): 

162 """Call processTree for @rst and @slides node p's subtree or p's ancestors.""" 

163 

164 def predicate(p): 

165 return self.is_rst_node(p) or g.match_word(p.h, 0, '@slides') 

166 

167 roots = g.findRootsWithPredicate(self.c, p, predicate=predicate) 

168 if roots: 

169 for p in roots: 

170 self.processTree(p) 

171 else: 

172 g.warning('No @rst or @slides nodes in', p.h) 

173 #@+node:ekr.20090502071837.63: *5* rst.processTree 

174 def processTree(self, root): 

175 """Process all @rst nodes in a tree.""" 

176 for p in root.self_and_subtree(): 

177 if self.is_rst_node(p): 

178 if self.in_rst_tree(p): 

179 g.trace(f"ignoring nested @rst node: {p.h}") 

180 else: 

181 h = p.h.strip() 

182 fn = h[4:].strip() 

183 if fn: 

184 source = self.write_rst_tree(p, fn) 

185 self.write_docutils_files(fn, p, source) 

186 elif g.match_word(h, 0, "@slides"): 

187 if self.in_slides_tree(p): 

188 g.trace(f"ignoring nested @slides node: {p.h}") 

189 else: 

190 self.write_slides(p) 

191 

192 #@+node:ekr.20090502071837.64: *5* rst.write_rst_tree 

193 def write_rst_tree(self, p, fn): 

194 """Convert p's tree to rst sources.""" 

195 c = self.c 

196 self.root = p.copy() 

197 # 

198 # Init encoding and path. 

199 d = c.scanAllDirectives(p) 

200 self.encoding = d.get('encoding') or 'utf-8' 

201 self.path = d.get('path') or '' 

202 # Write the output to self.result_list. 

203 self.result_list = [] # All output goes here. 

204 if self.generate_rst_header_comment: 

205 self.result_list.append(f".. rst3: filename: {fn}") 

206 for p in self.root.self_and_subtree(): 

207 self.writeNode(p) 

208 source = self.compute_result() 

209 return source 

210 

211 #@+node:ekr.20100822092546.5835: *5* rst.write_slides & helper 

212 def write_slides(self, p): 

213 """Convert p's children to slides.""" 

214 c = self.c 

215 p = p.copy() 

216 h = p.h 

217 i = g.skip_id(h, 1) # Skip the '@' 

218 kind, fn = h[:i].strip(), h[i:].strip() 

219 if not fn: 

220 g.error(f"{kind} requires file name") 

221 return 

222 title = p.firstChild().h if p and p.firstChild() else '<no slide>' 

223 title = title.strip().capitalize() 

224 n_tot = p.numberOfChildren() 

225 n = 1 

226 d = c.scanAllDirectives(p) 

227 self.encoding = d.get('encoding') or 'utf-8' 

228 self.path = d.get('path') or '' 

229 for child in p.children(): 

230 # Compute the slide's file name. 

231 fn2, ext = g.os_path_splitext(fn) 

232 fn2 = f"{fn2}-{n:03d}{ext}" # Use leading zeros for :glob:. 

233 n += 1 

234 # Write the rst sources. 

235 self.result_list = [] 

236 self.writeSlideTitle(title, n - 1, n_tot) 

237 self.result_list.append(child.b) 

238 source = self.compute_result() 

239 self.write_docutils_files(fn2, p, source) 

240 #@+node:ekr.20100822174725.5836: *6* rst.writeSlideTitle 

241 def writeSlideTitle(self, title, n, n_tot): 

242 """Write the title, underlined with the '#' character.""" 

243 if n != 1: 

244 title = f"{title} ({n} of {n_tot})" 

245 width = max(4, len(g.toEncodedString(title, 

246 encoding=self.encoding, reportErrors=False))) 

247 self.result_list.append(f"{title}\n{'#' * width}") 

248 #@+node:ekr.20090502071837.85: *5* rst.writeNode & helper 

249 def writeNode(self, p): 

250 """Append the rst srouces to self.result_list.""" 

251 c = self.c 

252 if self.is_ignore_node(p) or self.in_ignore_tree(p): 

253 return 

254 if g.match_word(p.h, 0, '@rst-no-head'): 

255 self.result_list.append(self.filter_b(c, p)) 

256 else: 

257 self.http_addNodeMarker(p) 

258 if p != self.root: 

259 self.result_list.append(self.underline(p, self.filter_h(c, p))) 

260 self.result_list.append(self.filter_b(c, p)) 

261 #@+node:ekr.20090502071837.96: *6* rst.http_addNodeMarker 

262 def http_addNodeMarker(self, p): 

263 """ 

264 Add a node marker for the mod_http plugin (HtmlParserClass class). 

265 

266 The first three elements are a stack of tags, the rest is html code:: 

267 

268 [ 

269 <tag n start>, <tag n end>, <other stack elements>, 

270 <html line 1>, <html line 2>, ... 

271 ] 

272 

273 <other stack elements> has the same structure:: 

274 

275 [<tag n-1 start>, <tag n-1 end>, <other stack elements>] 

276 """ 

277 if self.http_server_support: 

278 self.nodeNumber += 1 

279 anchorname = f"{self.node_begin_marker}{self.nodeNumber}" 

280 self.result_list.append(f".. _{anchorname}:") 

281 self.http_map[anchorname] = p.copy() 

282 #@+node:ekr.20100813041139.5919: *4* rst.write_docutils_files & helpers 

283 def write_docutils_files(self, fn, p, source): 

284 """Write source to the intermediate file and write the output from docutils..""" 

285 junk, ext = g.os_path_splitext(fn) 

286 ext = ext.lower() 

287 fn = self.computeOutputFileName(fn) 

288 ok = self.createDirectoryForFile(fn) 

289 if not ok: 

290 return 

291 # Write the intermediate file. 

292 if self.write_intermediate_file: 

293 self.writeIntermediateFile(fn, source) 

294 # Should we call docutils? 

295 if not self.call_docutils: 

296 return 

297 if ext not in ('.htm', '.html', '.tex', '.pdf', '.s5', '.odt'): # #1884: test now. 

298 return 

299 # Write the result from docutils. 

300 s = self.writeToDocutils(source, ext) 

301 if s and ext in ('.html', '.htm'): 

302 s = self.addTitleToHtml(s) 

303 if not s: 

304 return 

305 s = g.toEncodedString(s, 'utf-8') 

306 with open(fn, 'wb') as f: 

307 f.write(s) 

308 self.n_docutils += 1 

309 self.report(fn) 

310 #@+node:ekr.20100813041139.5913: *5* rst.addTitleToHtml 

311 def addTitleToHtml(self, s): 

312 """ 

313 Replace an empty <title> element by the contents of the first <h1> 

314 element. 

315 """ 

316 i = s.find('<title></title>') 

317 if i == -1: 

318 return s 

319 m = re.search(r'<h1>([^<]*)</h1>', s) 

320 if not m: 

321 m = re.search(r'<h1><[^>]+>([^<]*)</a></h1>', s) 

322 if m: 

323 s = s.replace('<title></title>', 

324 f"<title>{m.group(1)}</title>") 

325 return s 

326 #@+node:ekr.20090502071837.89: *5* rst.computeOutputFileName 

327 def computeOutputFileName(self, fn): 

328 """Return the full path to the output file.""" 

329 c = self.c 

330 openDirectory = c.frame.openDirectory 

331 if self.default_path: 

332 path = g.os_path_finalize_join(self.path, self.default_path, fn) 

333 elif self.path: 

334 path = g.os_path_finalize_join(self.path, fn) 

335 elif openDirectory: 

336 path = g.os_path_finalize_join(self.path, openDirectory, fn) 

337 else: 

338 path = g.os_path_finalize_join(fn) 

339 return path 

340 #@+node:ekr.20100813041139.5914: *5* rst.createDirectoryForFile 

341 def createDirectoryForFile(self, fn): 

342 """ 

343 Create the directory for fn if 

344 a) it doesn't exist and 

345 b) the user options allow it. 

346 

347 Return True if the directory existed or was made. 

348 """ 

349 c, ok = self.c, False # 1815. 

350 # Create the directory if it doesn't exist. 

351 theDir, junk = g.os_path_split(fn) 

352 theDir = g.os_path_finalize(theDir) # 1341 

353 if g.os_path_exists(theDir): 

354 return True 

355 if c and c.config and c.config.create_nonexistent_directories: 

356 theDir = c.expand_path_expression(theDir) 

357 ok = g.makeAllNonExistentDirectories(theDir) # type:ignore 

358 if not ok: 

359 g.error('did not create:', theDir) 

360 return ok 

361 #@+node:ekr.20100813041139.5912: *5* rst.writeIntermediateFile 

362 def writeIntermediateFile(self, fn, s): 

363 """Write s to to the file whose name is fn.""" 

364 # ext = self.getOption(p, 'write_intermediate_extension') 

365 ext = self.write_intermediate_extension 

366 if not ext.startswith('.'): 

367 ext = '.' + ext 

368 fn = fn + ext 

369 with open(fn, 'w', encoding=self.encoding) as f: 

370 f.write(s) 

371 self.n_intermediate += 1 

372 self.report(fn) 

373 #@+node:ekr.20090502071837.65: *5* rst.writeToDocutils & helper 

374 def writeToDocutils(self, s, ext): 

375 """Send s to docutils using the writer implied by ext and return the result.""" 

376 if not docutils: 

377 g.error('writeToDocutils: docutils not present') 

378 return None 

379 join = g.os_path_finalize_join 

380 openDirectory = self.c.frame.openDirectory 

381 overrides = {'output_encoding': self.encoding} 

382 # 

383 # Compute the args list if the stylesheet path does not exist. 

384 styleSheetArgsDict = self.handleMissingStyleSheetArgs() 

385 if ext == '.pdf': 

386 module = g.import_module('leo.plugins.leo_pdf') 

387 if not module: 

388 return None 

389 writer = module.Writer() # Get an instance. 

390 writer_name = None 

391 else: 

392 writer = None 

393 for ext2, writer_name in ( 

394 ('.html', 'html'), 

395 ('.htm', 'html'), 

396 ('.tex', 'latex'), 

397 ('.pdf', 'leo.plugins.leo_pdf'), 

398 ('.s5', 's5'), 

399 ('.odt', 'odt'), 

400 ): 

401 if ext2 == ext: 

402 break 

403 else: 

404 g.error(f"unknown docutils extension: {ext}") 

405 return None 

406 # 

407 # Make the stylesheet path relative to open directory. 

408 rel_stylesheet_path = self.stylesheet_path or '' 

409 stylesheet_path = join(openDirectory, rel_stylesheet_path) 

410 assert self.stylesheet_name 

411 path = join(self.stylesheet_path, self.stylesheet_name) 

412 if not self.stylesheet_embed: 

413 rel_path = join(rel_stylesheet_path, self.stylesheet_name) 

414 rel_path = rel_path.replace('\\', '/') 

415 overrides['stylesheet'] = rel_path 

416 overrides['stylesheet_path'] = None 

417 overrides['embed_stylesheet'] = None 

418 elif os.path.exists(path): 

419 if ext != '.pdf': 

420 overrides['stylesheet'] = path 

421 overrides['stylesheet_path'] = None 

422 elif styleSheetArgsDict: 

423 g.es_print('using publish_argv_for_missing_stylesheets', styleSheetArgsDict) 

424 overrides.update(styleSheetArgsDict) # MWC add args to settings 

425 elif rel_stylesheet_path == stylesheet_path: 

426 g.error(f"stylesheet not found: {path}") 

427 else: 

428 g.error('stylesheet not found\n', path) 

429 if self.path: 

430 g.es_print('@path:', self.path) 

431 g.es_print('open path:', openDirectory) 

432 if rel_stylesheet_path: 

433 g.es_print('relative path:', rel_stylesheet_path) 

434 try: 

435 result = None 

436 result = docutils.core.publish_string(source=s, 

437 reader_name='standalone', 

438 parser_name='restructuredtext', 

439 writer=writer, 

440 writer_name=writer_name, 

441 settings_overrides=overrides) 

442 if isinstance(result, bytes): 

443 result = g.toUnicode(result) 

444 except docutils.ApplicationError as error: 

445 g.error('Docutils error:') 

446 g.blue(error) 

447 except Exception: 

448 g.es_print('Unexpected docutils exception') 

449 g.es_exception() 

450 return result 

451 #@+node:ekr.20090502071837.66: *6* rst.handleMissingStyleSheetArgs 

452 def handleMissingStyleSheetArgs(self, s=None): 

453 """ 

454 Parse the publish_argv_for_missing_stylesheets option, 

455 returning a dict containing the parsed args. 

456 """ 

457 if 0: 

458 # See http://docutils.sourceforge.net/docs/user/config.html#documentclass 

459 return { 

460 'documentclass': 'report', 

461 'documentoptions': 'english,12pt,lettersize', 

462 } 

463 if not s: 

464 s = self.publish_argv_for_missing_stylesheets 

465 if not s: 

466 return {} 

467 # 

468 # Handle argument lists such as this: 

469 # --language=en,--documentclass=report,--documentoptions=[english,12pt,lettersize] 

470 d = {} 

471 while s: 

472 s = s.strip() 

473 if not s.startswith('--'): 

474 break 

475 s = s[2:].strip() 

476 eq = s.find('=') 

477 cm = s.find(',') 

478 if eq == -1 or (-1 < cm < eq): # key[nl] or key, 

479 val = '' 

480 cm = s.find(',') 

481 if cm == -1: 

482 key = s.strip() 

483 s = '' 

484 else: 

485 key = s[:cm].strip() 

486 s = s[cm + 1 :].strip() 

487 else: # key = val 

488 key = s[:eq].strip() 

489 s = s[eq + 1 :].strip() 

490 if s.startswith('['): # [...] 

491 rb = s.find(']') 

492 if rb == -1: 

493 break # Bad argument. 

494 val = s[: rb + 1] 

495 s = s[rb + 1 :].strip() 

496 if s.startswith(','): 

497 s = s[1:].strip() 

498 else: # val[nl] or val, 

499 cm = s.find(',') 

500 if cm == -1: 

501 val = s 

502 s = '' 

503 else: 

504 val = s[:cm].strip() 

505 s = s[cm + 1 :].strip() 

506 if not key: 

507 break 

508 if not val.strip(): 

509 val = '1' 

510 d[str(key)] = str(val) 

511 return d 

512 #@+node:ekr.20090512153903.5803: *4* rst.writeAtAutoFile & helpers 

513 def writeAtAutoFile(self, p, fileName, outputFile): 

514 """ 

515 at.writeAtAutoContents calls this method to write an @auto tree 

516 containing imported rST code. 

517 

518 at.writeAtAutoContents will close the output file. 

519 """ 

520 self.result_list = [] 

521 self.initAtAutoWrite(p) 

522 self.root = p.copy() 

523 after = p.nodeAfterTree() 

524 if not self.isSafeWrite(p): 

525 return False 

526 try: 

527 self.at_auto_write = True # Set the flag for underline. 

528 p = p.firstChild() # A hack: ignore the root node. 

529 while p and p != after: 

530 self.writeNode(p) # side effect: advances p 

531 s = self.compute_result() 

532 outputFile.write(s) 

533 ok = True 

534 except Exception: 

535 ok = False 

536 finally: 

537 self.at_auto_write = False 

538 return ok 

539 #@+node:ekr.20090513073632.5733: *5* rst.initAtAutoWrite 

540 def initAtAutoWrite(self, p): 

541 """Init underlining for for an @auto write.""" 

542 # User-defined underlining characters make no sense in @auto-rst. 

543 d = p.v.u.get('rst-import', {}) 

544 underlines2 = d.get('underlines2', '') 

545 # 

546 # Do *not* set a default for overlining characters. 

547 if len(underlines2) > 1: 

548 underlines2 = underlines2[0] 

549 g.warning(f"too many top-level underlines, using {underlines2}") 

550 underlines1 = d.get('underlines1', '') 

551 # 

552 # Pad underlines with default characters. 

553 default_underlines = '=+*^~"\'`-:><_' 

554 if underlines1: 

555 for ch in default_underlines[1:]: 

556 if ch not in underlines1: 

557 underlines1 = underlines1 + ch 

558 else: 

559 underlines1 = default_underlines 

560 self.at_auto_underlines = underlines2 + underlines1 

561 self.underlines1 = underlines1 

562 self.underlines2 = underlines2 

563 #@+node:ekr.20210401155057.7: *5* rst.isSafeWrite 

564 def isSafeWrite(self, p): 

565 """ 

566 Return True if node p contributes nothing but 

567 rst-options to the write. 

568 """ 

569 lines = g.splitLines(p.b) 

570 for z in lines: 

571 if z.strip() and not z.startswith('@') and not z.startswith('.. '): 

572 # A real line that will not be written. 

573 g.error('unsafe @auto-rst') 

574 g.es('body text will be ignored in\n', p.h) 

575 return False 

576 return True 

577 #@+node:ekr.20090502071837.67: *4* rst.writeNodeToString 

578 def writeNodeToString(self, p): 

579 """ 

580 rst.writeNodeToString: A utility for scripts. Not used in Leo. 

581 

582 Write p's tree to a string as if it were an @rst node. 

583 Return the string. 

584 """ 

585 return self.write_rst_tree(p, fn=p.h) 

586 #@+node:ekr.20210329105456.1: *3* rst: Filters 

587 #@+node:ekr.20210329105948.1: *4* rst.filter_b & self.filter_h 

588 def filter_b(self, c, p): 

589 """ 

590 Filter p.b with user_filter_b function. 

591 Don't allow filtering when in the @auto-rst logic. 

592 """ 

593 if self.user_filter_b and not self.at_auto_write: 

594 try: 

595 # pylint: disable=not-callable 

596 return self.user_filter_b(c, p) 

597 except Exception: 

598 g.es_exception() 

599 self.user_filter_b = None 

600 return p.b 

601 

602 def filter_h(self, c, p): 

603 """ 

604 Filter p.h with user_filter_h function. 

605 Don't allow filtering when in the @auto-rst logic. 

606 """ 

607 if self.user_filter_h and not self.at_auto_write: 

608 try: 

609 # pylint: disable=not-callable 

610 return self.user_filter_h(c, p) 

611 except Exception: 

612 g.es_exception() 

613 self.user_filter_h = None 

614 return p.h 

615 #@+node:ekr.20210329111528.1: *4* rst.register_*_filter 

616 def register_body_filter(self, f): 

617 """Register the user body filter.""" 

618 self.user_filter_b = f 

619 

620 def register_headline_filter(self, f): 

621 """Register the user headline filter.""" 

622 self.user_filter_h = f 

623 #@+node:ekr.20210331084407.1: *3* rst: Predicates 

624 def in_ignore_tree(self, p): 

625 return any(g.match_word(p2.h, 0, '@rst-ignore-tree') 

626 for p2 in self.rst_parents(p)) 

627 

628 def in_rst_tree(self, p): 

629 return any(self.is_rst_node(p2) for p2 in self.rst_parents(p)) 

630 

631 def in_slides_tree(self, p): 

632 return any(g.match_word(p.h, 0, "@slides") for p2 in self.rst_parents(p)) 

633 

634 def is_ignore_node(self, p): 

635 return g.match_words(p.h, 0, ('@rst-ignore', '@rst-ignore-node')) 

636 

637 def is_rst_node(self, p): 

638 return g.match_word(p.h, 0, "@rst") and not g.match(p.h, 0, "@rst-") 

639 

640 def rst_parents(self, p): 

641 for p2 in p.parents(): 

642 if p2 == self.root: 

643 return 

644 yield p2 

645 #@+node:ekr.20090502071837.88: *3* rst: Utils 

646 #@+node:ekr.20210326165315.1: *4* rst.compute_result 

647 def compute_result(self): 

648 """Concatenate all strings in self.result, ensuring exactly one blank line between strings.""" 

649 return ''.join(f"{s.rstrip()}\n\n" for s in self.result_list if s.strip()) 

650 #@+node:ekr.20090502071837.43: *4* rst.dumpDict 

651 def dumpDict(self, d, tag): 

652 """Dump the given settings dict.""" 

653 g.pr(tag + '...') 

654 for key in sorted(d): 

655 g.pr(f" {key:20} {d.get(key)}") 

656 #@+node:ekr.20090502071837.90: *4* rst.encode 

657 def encode(self, s): 

658 """return s converted to an encoded string.""" 

659 return g.toEncodedString(s, encoding=self.encoding, reportErrors=True) 

660 #@+node:ekr.20090502071837.91: *4* rst.report 

661 def report(self, name): 

662 """Issue a report to the log pane.""" 

663 if self.silent: 

664 return 

665 name = g.os_path_finalize(name) # 1341 

666 g.pr(f"wrote: {name}") 

667 #@+node:ekr.20090502071837.92: *4* rst.rstComment 

668 def rstComment(self, s): 

669 return f".. {s}" 

670 #@+node:ekr.20090502071837.93: *4* rst.underline 

671 def underline(self, p, s): 

672 """ 

673 Return the underlining string to be used at the given level for string s. 

674 This includes the headline, and possibly a leading overlining line. 

675 """ 

676 # Never add the root's headline. 

677 if not s: 

678 return '' 

679 encoded_s = g.toEncodedString(s, encoding=self.encoding, reportErrors=False) 

680 if self.at_auto_write: 

681 # We *might* generate overlines for top-level sections. 

682 u = self.at_auto_underlines 

683 level = p.level() - self.root.level() 

684 # This is tricky. The index n depends on several factors. 

685 if self.underlines2: 

686 level -= 1 # There *is* a double-underlined section. 

687 n = level 

688 else: 

689 n = level - 1 

690 if 0 <= n < len(u): 

691 ch = u[n] 

692 elif u: 

693 ch = u[-1] 

694 else: 

695 g.trace('can not happen: no u') 

696 ch = '#' 

697 # Write longer underlines for non-ascii characters. 

698 n = max(4, len(encoded_s)) 

699 if level == 0 and self.underlines2: 

700 # Generate an overline and an underline. 

701 return f"{ch * n}\n{p.h}\n{ch * n}" 

702 # Generate only an underline. 

703 return f"{p.h}\n{ch * n}" 

704 # 

705 # The user is responsible for top-level overlining. 

706 u = self.underline_characters # '''#=+*^~"'`-:><_''' 

707 level = max(0, p.level() - self.root.level()) 

708 level = min(level + 1, len(u) - 1) # Reserve the first character for explicit titles. 

709 ch = u[level] 

710 n = max(4, len(encoded_s)) 

711 return f"{s.strip()}\n{ch * n}" 

712 #@-others 

713#@-others 

714#@@language python 

715#@@tabwidth -4 

716#@@pagewidth 70 

717#@-leo