Coverage for C:\leo.repo\leo-editor\leo\core\leoMarkup.py: 22%

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

246 statements  

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 

16 

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. 

33 

34 Each @adoc node should have the form: `@adoc x.adoc`. Relative file names 

35 are relative to the base directory. See below. 

36 

37 By default, the adoc command creates AsciiDoctor headings from Leo 

38 headlines. However, the following kinds of nodes are treated differently: 

39 

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. 

43 

44 After running the adoc command, use the asciidoctor tool to convert the 

45 x.adoc files to x.html. 

46 

47 Settings 

48 -------- 

49 

50 AciiDoctor markup provides many settings, including:: 

51 

52 = Title 

53 :stylesdir: mystylesheets/ 

54 :stylesheet: mystyles.css 

55 

56 These can also be specified on the command line:: 

57 

58 asciidoctor -a stylesdir=mystylesheets/ -a stylesheet=mystyles.css 

59 

60 @string adoc-base-directory specifies the base for relative file names. 

61 The default is c.frame.openDirectory 

62 

63 Scripting interface 

64 ------------------- 

65 

66 Scripts may invoke the adoc command as follows:: 

67 

68 event = g.Bunch(base_dicrectory=my_directory, p=some_node) 

69 c.markupCommands.adoc_command(event=event) 

70 

71 This @button node runs the adoc command and coverts all results to .html:: 

72 

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]) 

78 

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) 

85 

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. 

102 

103 Each @pandoc node should have the form: `@pandoc x.adoc`. Relative file names 

104 are relative to the base directory. See below. 

105 

106 By default, the pandoc command creates AsciiDoctor headings from Leo 

107 headlines. However, the following kinds of nodes are treated differently: 

108 

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. 

112 

113 After running the pandoc command, use the pandoc tool to convert the x.adoc 

114 files to x.html. 

115 

116 Settings 

117 -------- 

118 

119 @string pandoc-base-directory specifies the base for relative file names. 

120 The default is c.frame.openDirectory 

121 

122 Scripting interface 

123 ------------------- 

124 

125 Scripts may invoke the adoc command as follows:: 

126 

127 event = g.Bunch(base_dicrectory=my_directory, p=some_node) 

128 c.markupCommands.pandoc_command(event=event) 

129 

130 This @button node runs the adoc command and coverts all results to .html:: 

131 

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]) 

137 

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) 

144 

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. 

161 

162 Each @sphinx node should have the form: `@sphinx x`. Relative file names 

163 are relative to the base directory. See below. 

164 

165 By default, the sphinx command creates Sphinx headings from Leo headlines. 

166 However, the following kinds of nodes are treated differently: 

167 

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. 

171 

172 After running the sphinx command, use the sphinx tool to convert the 

173 output files to x.html. 

174 

175 Settings 

176 -------- 

177 

178 @string sphinx-base-directory specifies the base for relative file names. 

179 The default is c.frame.openDirectory 

180 

181 Scripting interface 

182 ------------------- 

183 

184 Scripts may invoke the sphinx command as follows:: 

185 

186 event = g.Bunch(base_dicrectory=my_directory, p=some_node) 

187 c.markupCommands.sphinx_command(event=event) 

188 

189 This @button node runs the sphinx command and coverts all results to .html:: 

190 

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]) 

196 

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) 

203 

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.""" 

214 

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() 

221 

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') 

229 

230 #@+others 

231 #@+node:ekr.20191006153233.1: *3* markup.command_helper & helpers 

232 def command_helper(self, event, kind, preview, verbose): 

233 

234 def predicate(p): 

235 return self.filename(p) 

236 

237 # Find all roots. 

238 

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)') 

294 

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'^= ') 

414 

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 [] 

476 

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 [] 

485 

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