Coverage for C:\leo.repo\leo-editor\leo\core\leoRst.py: 44%
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
Shortcuts on this page
r m x toggle line displays
j k next/prev highlighted chunk
0 (zero) top of page
1 (one) first highlighted chunk
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.
9For full documentation, see: http://leoeditor.com/rstplugin3.html
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)
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."""
164 def predicate(p):
165 return self.is_rst_node(p) or g.match_word(p.h, 0, '@slides')
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)
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
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).
266 The first three elements are a stack of tags, the rest is html code::
268 [
269 <tag n start>, <tag n end>, <other stack elements>,
270 <html line 1>, <html line 2>, ...
271 ]
273 <other stack elements> has the same structure::
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.
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.
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.
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
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
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))
628 def in_rst_tree(self, p):
629 return any(self.is_rst_node(p2) for p2 in self.rst_parents(p))
631 def in_slides_tree(self, p):
632 return any(g.match_word(p.h, 0, "@slides") for p2 in self.rst_parents(p))
634 def is_ignore_node(self, p):
635 return g.match_words(p.h, 0, ('@rst-ignore', '@rst-ignore-node'))
637 def is_rst_node(self, p):
638 return g.match_word(p.h, 0, "@rst") and not g.match(p.h, 0, "@rst-")
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