Coverage for C:\Repos\leo-editor\leo\core\leoMarkup.py: 22%
246 statements
« prev ^ index » next coverage.py v6.4, created at 2022-05-24 10:21 -0500
« 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.20190515070742.1: * @file leoMarkup.py
4#@@first
5"""Supports @adoc, @pandoc and @sphinx nodes and related commands."""
6#@+<< leoMarkup imports >>
7#@+node:ekr.20190515070742.3: ** << leoMarkup imports >>
8import io
9from shutil import which
10import os
11import re
12import time
13import leo.core.leoGlobals as g
14# Abbreviation.
15StringIO = io.StringIO
17#@-<< leoMarkup imports >>
18asciidoctor_exec = which('asciidoctor')
19asciidoc3_exec = which('asciidoc3')
20pandoc_exec = which('pandoc')
21sphinx_build = which('sphinx-build')
22#@+others
23#@+node:ekr.20191006153522.1: ** adoc, pandoc & sphinx commands
24#@+node:ekr.20190515070742.22: *3* @g.command: 'adoc' & 'adoc-with-preview')
25@g.command('adoc')
26def adoc_command(event=None, verbose=True):
27 #@+<< adoc command docstring >>
28 #@+node:ekr.20190515115100.1: *4* << adoc command docstring >>
29 """
30 The adoc command writes all @adoc nodes in the selected tree to the
31 files given in each @doc node. If no @adoc nodes are found, the
32 command looks up the tree.
34 Each @adoc node should have the form: `@adoc x.adoc`. Relative file names
35 are relative to the base directory. See below.
37 By default, the adoc command creates AsciiDoctor headings from Leo
38 headlines. However, the following kinds of nodes are treated differently:
40 - @ignore-tree: Ignore the node and its descendants.
41 - @ignore-node: Ignore the node.
42 - @no-head: Ignore the headline. Do not generate a heading.
44 After running the adoc command, use the asciidoctor tool to convert the
45 x.adoc files to x.html.
47 Settings
48 --------
50 AciiDoctor markup provides many settings, including::
52 = Title
53 :stylesdir: mystylesheets/
54 :stylesheet: mystyles.css
56 These can also be specified on the command line::
58 asciidoctor -a stylesdir=mystylesheets/ -a stylesheet=mystyles.css
60 @string adoc-base-directory specifies the base for relative file names.
61 The default is c.frame.openDirectory
63 Scripting interface
64 -------------------
66 Scripts may invoke the adoc command as follows::
68 event = g.Bunch(base_dicrectory=my_directory, p=some_node)
69 c.markupCommands.adoc_command(event=event)
71 This @button node runs the adoc command and coverts all results to .html::
73 import os
74 paths = c.markupCommands.adoc_command(event=g.Bunch(p=p))
75 paths = [z.replace('/', os.path.sep) for z in paths]
76 input_paths = ' '.join(paths)
77 g.execute_shell_commands(['asciidoctor %s' % input_paths])
79 """
80 #@-<< adoc command docstring >>
81 c = event and event.get('c')
82 if not c:
83 return None
84 return c.markupCommands.adoc_command(event, preview=False, verbose=verbose)
86@g.command('adoc-with-preview')
87def adoc_with_preview_command(event=None, verbose=True):
88 """Run the adoc command, then show the result in the browser."""
89 c = event and event.get('c')
90 if not c:
91 return None
92 return c.markupCommands.adoc_command(event, preview=True, verbose=verbose)
93#@+node:ekr.20191006153411.1: *3* @g.command: 'pandoc' & 'pandoc-with-preview'
94@g.command('pandoc')
95def pandoc_command(event, verbose=True):
96 #@+<< pandoc command docstring >>
97 #@+node:ekr.20191006153547.1: *4* << pandoc command docstring >>
98 """
99 The pandoc command writes all @pandoc nodes in the selected tree to the
100 files given in each @pandoc node. If no @pandoc nodes are found, the
101 command looks up the tree.
103 Each @pandoc node should have the form: `@pandoc x.adoc`. Relative file names
104 are relative to the base directory. See below.
106 By default, the pandoc command creates AsciiDoctor headings from Leo
107 headlines. However, the following kinds of nodes are treated differently:
109 - @ignore-tree: Ignore the node and its descendants.
110 - @ignore-node: Ignore the node.
111 - @no-head: Ignore the headline. Do not generate a heading.
113 After running the pandoc command, use the pandoc tool to convert the x.adoc
114 files to x.html.
116 Settings
117 --------
119 @string pandoc-base-directory specifies the base for relative file names.
120 The default is c.frame.openDirectory
122 Scripting interface
123 -------------------
125 Scripts may invoke the adoc command as follows::
127 event = g.Bunch(base_dicrectory=my_directory, p=some_node)
128 c.markupCommands.pandoc_command(event=event)
130 This @button node runs the adoc command and coverts all results to .html::
132 import os
133 paths = c.markupCommands.pandoc_command(event=g.Bunch(p=p))
134 paths = [z.replace('/', os.path.sep) for z in paths]
135 input_paths = ' '.join(paths)
136 g.execute_shell_commands(['asciidoctor %s' % input_paths])
138 """
139 #@-<< pandoc command docstring >>
140 c = event and event.get('c')
141 if not c:
142 return None
143 return c.markupCommands.pandoc_command(event, verbose=verbose)
145@g.command('pandoc-with-preview')
146def pandoc_with_preview_command(event=None, verbose=True):
147 """Run the pandoc command, then show the result in the browser."""
148 c = event and event.get('c')
149 if not c:
150 return None
151 return c.markupCommands.pandoc_command(event, preview=True, verbose=verbose)
152#@+node:ekr.20191017163422.1: *3* @g.command: 'sphinx' & 'sphinx-with-preview'
153@g.command('sphinx')
154def sphinx_command(event, verbose=True):
155 #@+<< sphinx command docstring >>
156 #@+node:ekr.20191017163422.2: *4* << sphinx command docstring >>
157 """
158 The sphinx command writes all @sphinx nodes in the selected tree to the
159 files given in each @sphinx node. If no @sphinx nodes are found, the
160 command looks up the tree.
162 Each @sphinx node should have the form: `@sphinx x`. Relative file names
163 are relative to the base directory. See below.
165 By default, the sphinx command creates Sphinx headings from Leo headlines.
166 However, the following kinds of nodes are treated differently:
168 - @ignore-tree: Ignore the node and its descendants.
169 - @ignore-node: Ignore the node.
170 - @no-head: Ignore the headline. Do not generate a heading.
172 After running the sphinx command, use the sphinx tool to convert the
173 output files to x.html.
175 Settings
176 --------
178 @string sphinx-base-directory specifies the base for relative file names.
179 The default is c.frame.openDirectory
181 Scripting interface
182 -------------------
184 Scripts may invoke the sphinx command as follows::
186 event = g.Bunch(base_dicrectory=my_directory, p=some_node)
187 c.markupCommands.sphinx_command(event=event)
189 This @button node runs the sphinx command and coverts all results to .html::
191 import os
192 paths = c.markupCommands.sphinx_command(event=g.Bunch(p=p))
193 paths = [z.replace('/', os.path.sep) for z in paths]
194 input_paths = ' '.join(paths)
195 g.execute_shell_commands(['asciidoctor %s' % input_paths])
197 """
198 #@-<< sphinx command docstring >>
199 c = event and event.get('c')
200 if not c:
201 return None
202 return c.markupCommands.sphinx_command(event, verbose=verbose)
204@g.command('sphinx-with-preview')
205def sphinx_with_preview_command(event=None, verbose=True):
206 """Run the sphinx command, then show the result in the browser."""
207 c = event and event.get('c')
208 if not c:
209 return None
210 return c.markupCommands.sphinx_command(event, preview=True, verbose=verbose)
211#@+node:ekr.20191006154236.1: ** class MarkupCommands
212class MarkupCommands:
213 """A class to write AsiiDoctor or docutils markup in Leo outlines."""
215 def __init__(self, c):
216 self.c = c
217 self.kind = None # 'adoc' or 'pandoc'
218 self.level_offset = 0
219 self.root_level = 0
220 self.reload_settings()
222 def reload_settings(self):
223 c = self.c
224 getString = c.config.getString
225 self.sphinx_command_dir = getString('sphinx-command-directory')
226 self.sphinx_default_command = getString('sphinx-default-command')
227 self.sphinx_input_dir = getString('sphinx-input-directory')
228 self.sphinx_output_dir = getString('sphinx-output-directory')
230 #@+others
231 #@+node:ekr.20191006153233.1: *3* markup.command_helper & helpers
232 def command_helper(self, event, kind, preview, verbose):
234 def predicate(p):
235 return self.filename(p)
237 # Find all roots.
239 t1 = time.time()
240 c = self.c
241 self.kind = kind
242 p = event.p if event and hasattr(event, 'p') else c.p
243 roots = g.findRootsWithPredicate(c, p, predicate=predicate)
244 if not roots:
245 g.warning('No @adoc nodes in', p.h)
246 return []
247 # Write each root to a file.
248 i_paths = []
249 for p in roots:
250 try:
251 i_path = self.filename(p)
252 # #1398.
253 i_path = c.expand_path_expression(i_path)
254 i_path = g.os_path_finalize(i_path)
255 with open(i_path, 'w', encoding='utf-8', errors='replace') as self.output_file:
256 self.write_root(p)
257 i_paths.append(i_path)
258 except IOError:
259 g.es_print(f"Can not open {i_path!r}")
260 except Exception:
261 g.es_print(f"Unexpected exception opening {i_path!r}")
262 g.es_exception()
263 # Convert each file to html.
264 o_paths = []
265 for i_path in i_paths:
266 o_path = self.compute_opath(i_path)
267 o_paths.append(o_path)
268 if kind == 'adoc':
269 self.run_asciidoctor(i_path, o_path)
270 elif kind == 'pandoc':
271 self.run_pandoc(i_path, o_path)
272 elif kind == 'sphinx':
273 self.run_sphinx(i_path, o_path)
274 else:
275 g.trace('BAD KIND')
276 return None
277 if kind != 'sphinx':
278 print(f"{kind}: wrote {o_path}")
279 if preview:
280 if kind == 'sphinx':
281 g.es_print('preview not available for sphinx')
282 else:
283 # open .html files in the default browser.
284 g.execute_shell_commands(o_paths)
285 t2 = time.time()
286 if verbose:
287 n = len(i_paths)
288 g.es_print(
289 f"{kind}: wrote {n} file{g.plural(n)} "
290 f"in {(t2-t1):4.2f} sec.")
291 return i_paths
292 #@+node:ekr.20190515084219.1: *4* markup.filename
293 adoc_pattern = re.compile(r'^@(adoc|asciidoctor)')
295 def filename(self, p):
296 """Return the filename of the @adoc, @pandoc or @sphinx node, or None."""
297 kind = self.kind
298 h = p.h.rstrip()
299 if kind == 'adoc':
300 m = self.adoc_pattern.match(h)
301 if m:
302 prefix = m.group(1)
303 return h[1 + len(prefix) :].strip()
304 return None
305 if kind in ('pandoc', 'sphinx'):
306 prefix = f"@{kind}"
307 if g.match_word(h, 0, prefix):
308 return h[len(prefix) :].strip()
309 return None
310 g.trace('BAD KIND', kind)
311 return None
312 #@+node:ekr.20191007053522.1: *4* markup.compute_opath
313 def compute_opath(self, i_path):
314 """
315 Neither asciidoctor nor pandoc handles extra extentions well.
316 """
317 c = self.c
318 for i in range(3):
319 i_path, ext = os.path.splitext(i_path)
320 if not ext:
321 break
322 # #1373.
323 base_dir = os.path.dirname(c.fileName())
324 return g.os_path_finalize_join(base_dir, i_path + '.html')
325 #@+node:ekr.20191007043110.1: *4* markup.run_asciidoctor
326 def run_asciidoctor(self, i_path, o_path):
327 """
328 Process the input file given by i_path with asciidoctor or asciidoc3.
329 """
330 global asciidoctor_exec, asciidoc3_exec
331 assert asciidoctor_exec or asciidoc3_exec, g.callers()
332 # Call the external program to write the output file.
333 # The -e option deletes css.
334 prog = 'asciidoctor' if asciidoctor_exec else 'asciidoc3'
335 command = f"{prog} {i_path} -o {o_path} -b html5"
336 g.execute_shell_commands(command)
337 #@+node:ekr.20191007043043.1: *4* markup.run_pandoc
338 def run_pandoc(self, i_path, o_path):
339 """
340 Process the input file given by i_path with pandoc.
341 """
342 global pandoc_exec
343 assert pandoc_exec, g.callers()
344 # Call pandoc to write the output file.
345 # --quiet does no harm.
346 command = f"pandoc {i_path} -t html5 -o {o_path}"
347 g.execute_shell_commands(command)
348 #@+node:ekr.20191017165427.1: *4* markup.run_sphinx
349 def run_sphinx(self, i_path, o_path):
350 """Process i_path and o_path with sphinx."""
351 trace = True
352 # cd to the command directory, or i_path's directory.
353 command_dir = g.os_path_finalize(
354 self.sphinx_command_dir or os.path.dirname(i_path))
355 if os.path.exists(command_dir):
356 if trace:
357 g.trace(f"\nos.chdir: {command_dir!r}")
358 os.chdir(command_dir)
359 else:
360 g.error(f"command directory not found: {command_dir!r}")
361 return
362 #
363 # If a default command exists, just call it.
364 # The user is responsible for making everything work.
365 if self.sphinx_default_command:
366 if trace:
367 g.trace(f"\ncommand: {self.sphinx_default_command!r}\n")
368 g.execute_shell_commands(self.sphinx_default_command)
369 return
370 # Compute the input directory.
371 input_dir = g.os_path_finalize(
372 self.sphinx_input_dir or os.path.dirname(i_path))
373 if not os.path.exists(input_dir):
374 g.error(f"input directory not found: {input_dir!r}")
375 return
376 # Compute the output directory.
377 output_dir = g.os_path_finalize(
378 self.sphinx_output_dir or os.path.dirname(o_path))
379 if not os.path.exists(output_dir):
380 g.error(f"output directory not found: {output_dir!r}")
381 return
382 #
383 # Call sphinx-build to write the output file.
384 # sphinx-build [OPTIONS] SOURCEDIR OUTPUTDIR [FILENAMES...]
385 command = f"sphinx-build {input_dir} {output_dir} {i_path}"
386 if trace:
387 g.trace(f"\ncommand: {command!r}\n")
388 g.execute_shell_commands(command)
389 #@+node:ekr.20190515070742.24: *3* markup.write_root & helpers
390 def write_root(self, root):
391 """Process all nodes in an @adoc tree to self.output_file"""
392 # Write only the body of the root.
393 self.write_body(root)
394 # Write all nodes of the tree, except ignored nodes.
395 self.level_offset = self.compute_level_offset(root)
396 self.root_level = root.level()
397 p = root.threadNext() # Returns a copy.
398 after = root.nodeAfterTree()
399 while p and p != after:
400 h = p.h.rstrip()
401 if g.match_word(h, 0, '@ignore-tree'):
402 p.moveToNodeAfterTree()
403 continue
404 if g.match_word(h, 0, '@ignore-node'):
405 p.moveToThreadNext()
406 continue
407 if not g.match_word(h, 0, '@no-head'):
408 self.write_headline(p)
409 self.write_body(p)
410 p.moveToThreadNext()
411 #@+node:ekr.20190515114836.1: *4* markup.compute_level_offset
412 adoc_title_pat = re.compile(r'^= ')
413 pandoc_title_pat = re.compile(r'^= ')
415 def compute_level_offset(self, root):
416 """
417 Return 1 if the root.b contains a title. Otherwise 0.
418 """
419 pattern = self.adoc_title_pat if self.kind == 'adoc' else self.pandoc_title_pat
420 for line in g.splitLines(root.b):
421 if pattern.match(line):
422 return 1
423 return 0
424 #@+node:ekr.20190515070742.38: *4* markup.write_body
425 def write_body(self, p):
426 """Write p.b"""
427 # We no longer add newlines to the start of nodes because
428 # we write a blank line after all sections.
429 s = self.remove_directives(p.b)
430 self.output_file.write(g.ensureTrailingNewlines(s, 2))
431 #@+node:ekr.20190515070742.47: *4* markup.write_headline
432 def write_headline(self, p):
433 """Generate an AsciiDoctor section"""
434 if not p.h.strip():
435 return
436 level = max(0, self.level_offset + p.level() - self.root_level)
437 if self.kind == 'sphinx':
438 # For now, assume rST markup!
439 # Hard coded characters. Never use '#' underlining.
440 chars = '''=+*^~"'`-:><_'''
441 if len(chars) > level:
442 ch = chars[level]
443 line = ch * len(p.h)
444 self.output_file.write(f"{p.h}\n{line}\n\n")
445 return
446 if self.kind == 'pandoc':
447 section = '#' * min(level, 6)
448 elif self.kind == 'adoc':
449 # level 0 (a single #) should be done by hand.
450 section = '=' * level
451 else:
452 g.es_print(f"bad kind: {self.kind!r}")
453 return
454 self.output_file.write(f"{section} {p.h}\n\n")
455 #@+node:ekr.20191007054942.1: *4* markup.remove_directives
456 def remove_directives(self, s):
457 lines = g.splitLines(s)
458 result = []
459 for s in lines:
460 if s.startswith('@'):
461 i = g.skip_id(s, 1)
462 word = s[1:i]
463 if word in g.globalDirectiveList:
464 continue
465 result.append(s)
466 return ''.join(result)
467 #@+node:ekr.20191006155051.1: *3* markup.commands
468 def adoc_command(self, event=None, preview=False, verbose=True):
469 global asciidoctor_exec, asciidoc3_exec
470 if asciidoctor_exec or asciidoc3_exec:
471 return self.command_helper(
472 event, kind='adoc', preview=preview, verbose=verbose)
473 name = 'adoc-with-preview' if preview else 'adoc'
474 g.es_print(f"{name} requires either asciidoctor or asciidoc3")
475 return []
477 def pandoc_command(self, event=None, preview=False, verbose=True):
478 global pandoc_exec
479 if pandoc_exec:
480 return self.command_helper(
481 event, kind='pandoc', preview=preview, verbose=verbose)
482 name = 'pandoc-with-preview' if preview else 'pandoc'
483 g.es_print(f"{name} requires pandoc")
484 return []
486 def sphinx_command(self, event=None, preview=False, verbose=True):
487 global sphinx_build
488 if sphinx_build:
489 return self.command_helper(
490 event, kind='sphinx', preview=preview, verbose=verbose)
491 name = 'sphinx-with-preview' if preview else 'sphinx'
492 g.es_print(f"{name} requires sphinx")
493 return []
494 #@-others
495#@-others
496#@@language python
497#@@tabwidth -4
498#@@pagewidth 70
499#@-leo