1from __future__ import annotations 2 3import argparse 4import html 5import json 6import xml.sax.saxutils as xml 7 8from abc import abstractmethod 9from collections.abc import Mapping, Sequence 10from markdown_it.token import Token 11from typing import Any, Generic, Optional 12from urllib.parse import quote 13 14 15from . import md 16from . import parallel 17from .asciidoc import AsciiDocRenderer, asciidoc_escape 18from .commonmark import CommonMarkRenderer 19from .docbook import DocBookRenderer, make_xml_id 20from .html import HTMLRenderer 21from .manpage import ManpageRenderer, man_escape 22from .manual_structure import XrefTarget 23from .md import Converter, md_escape, md_make_code 24from .types import OptionLoc, Option, RenderedOption 25 26def option_is(option: Option, key: str, typ: str) -> Optional[dict[str, str]]: 27 if key not in option: 28 return None 29 if type(option[key]) != dict: 30 return None 31 if option[key].get('_type') != typ: # type: ignore[union-attr] 32 return None 33 return option[key] # type: ignore[return-value] 34 35class BaseConverter(Converter[md.TR], Generic[md.TR]): 36 __option_block_separator__: str 37 38 _options: dict[str, RenderedOption] 39 40 def __init__(self, revision: str): 41 super().__init__() 42 self._options = {} 43 self._revision = revision 44 45 def _sorted_options(self) -> list[tuple[str, RenderedOption]]: 46 keys = list(self._options.keys()) 47 keys.sort(key=lambda opt: [ (0 if p.startswith("enable") else 1 if p.startswith("package") else 2, p) 48 for p in self._options[opt].loc ]) 49 return [ (k, self._options[k]) for k in keys ] 50 51 def _format_decl_def_loc(self, loc: OptionLoc) -> tuple[Optional[str], str]: 52 # locations can be either plain strings (specific to nixpkgs), or attrsets 53 # { name = "foo/bar.nix"; url = "https://github.com/....."; } 54 if isinstance(loc, str): 55 # Hyperlink the filename either to the NixOS github 56 # repository (if it’s a module and we have a revision number), 57 # or to the local filesystem. 58 if not loc.startswith('/'): 59 if self._revision == 'local': 60 href = f"https://github.com/NixOS/nixpkgs/blob/master/{loc}" 61 else: 62 href = f"https://github.com/NixOS/nixpkgs/blob/{self._revision}/{loc}" 63 else: 64 href = f"file://{loc}" 65 # Print the filename and make it user-friendly by replacing the 66 # /nix/store/<hash> prefix by the default location of nixos 67 # sources. 68 if not loc.startswith('/'): 69 name = f"<nixpkgs/{loc}>" 70 elif 'nixops' in loc and '/nix/' in loc: 71 name = f"<nixops/{loc[loc.find('/nix/') + 5:]}>" 72 else: 73 name = loc 74 return (href, name) 75 else: 76 return (loc['url'] if 'url' in loc else None, loc['name']) 77 78 @abstractmethod 79 def _decl_def_header(self, header: str) -> list[str]: raise NotImplementedError() 80 81 @abstractmethod 82 def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: raise NotImplementedError() 83 84 @abstractmethod 85 def _decl_def_footer(self) -> list[str]: raise NotImplementedError() 86 87 def _render_decl_def(self, header: str, locs: list[OptionLoc]) -> list[str]: 88 result = [] 89 result += self._decl_def_header(header) 90 for loc in locs: 91 href, name = self._format_decl_def_loc(loc) 92 result += self._decl_def_entry(href, name) 93 result += self._decl_def_footer() 94 return result 95 96 def _render_code(self, option: Option, key: str) -> list[str]: 97 if lit := option_is(option, key, 'literalMD'): 98 return [ self._render(f"*{key.capitalize()}:*\n{lit['text']}") ] 99 elif lit := option_is(option, key, 'literalExpression'): 100 code = md_make_code(lit['text']) 101 return [ self._render(f"*{key.capitalize()}:*\n{code}") ] 102 elif key in option: 103 raise Exception(f"{key} has unrecognized type", option[key]) 104 else: 105 return [] 106 107 def _render_description(self, desc: str | dict[str, str]) -> list[str]: 108 if isinstance(desc, str): 109 return [ self._render(desc) ] if desc else [] 110 elif isinstance(desc, dict) and desc.get('_type') == 'mdDoc': 111 return [ self._render(desc['text']) ] if desc['text'] else [] 112 else: 113 raise Exception("description has unrecognized type", desc) 114 115 @abstractmethod 116 def _related_packages_header(self) -> list[str]: raise NotImplementedError() 117 118 def _convert_one(self, option: dict[str, Any]) -> list[str]: 119 blocks: list[list[str]] = [] 120 121 if desc := option.get('description'): 122 blocks.append(self._render_description(desc)) 123 if typ := option.get('type'): 124 ro = " *(read only)*" if option.get('readOnly', False) else "" 125 blocks.append([ self._render(f"*Type:*\n{md_escape(typ)}{ro}") ]) 126 127 if option.get('default'): 128 blocks.append(self._render_code(option, 'default')) 129 if option.get('example'): 130 blocks.append(self._render_code(option, 'example')) 131 132 if related := option.get('relatedPackages'): 133 blocks.append(self._related_packages_header()) 134 blocks[-1].append(self._render(related)) 135 if decl := option.get('declarations'): 136 blocks.append(self._render_decl_def("Declared by", decl)) 137 if defs := option.get('definitions'): 138 blocks.append(self._render_decl_def("Defined by", defs)) 139 140 for part in [ p for p in blocks[0:-1] if p ]: 141 part.append(self.__option_block_separator__) 142 143 return [ l for part in blocks for l in part ] 144 145 # this could return a TState parameter, but that does not allow dependent types and 146 # will cause headaches when using BaseConverter as a type bound anywhere. Any is the 147 # next best thing we can use, and since this is internal it will be mostly safe. 148 @abstractmethod 149 def _parallel_render_prepare(self) -> Any: raise NotImplementedError() 150 # this should return python 3.11's Self instead to ensure that a prepare+finish 151 # round-trip ends up with an object of the same type. for now we'll use BaseConverter 152 # since it's good enough so far. 153 @classmethod 154 @abstractmethod 155 def _parallel_render_init_worker(cls, a: Any) -> BaseConverter[md.TR]: raise NotImplementedError() 156 157 def _render_option(self, name: str, option: dict[str, Any]) -> RenderedOption: 158 try: 159 return RenderedOption(option['loc'], self._convert_one(option)) 160 except Exception as e: 161 raise Exception(f"Failed to render option {name}") from e 162 163 @classmethod 164 def _parallel_render_step(cls, s: BaseConverter[md.TR], a: Any) -> RenderedOption: 165 return s._render_option(*a) 166 167 def add_options(self, options: dict[str, Any]) -> None: 168 mapped = parallel.map(self._parallel_render_step, options.items(), 100, 169 self._parallel_render_init_worker, self._parallel_render_prepare()) 170 for (name, option) in zip(options.keys(), mapped): 171 self._options[name] = option 172 173 @abstractmethod 174 def finalize(self) -> str: raise NotImplementedError() 175 176class OptionDocsRestrictions: 177 def heading_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 178 raise RuntimeError("md token not supported in options doc", token) 179 def heading_close(self, token: Token, tokens: Sequence[Token], i: int) -> str: 180 raise RuntimeError("md token not supported in options doc", token) 181 def attr_span_begin(self, token: Token, tokens: Sequence[Token], i: int) -> str: 182 raise RuntimeError("md token not supported in options doc", token) 183 def example_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 184 raise RuntimeError("md token not supported in options doc", token) 185 186class OptionsDocBookRenderer(OptionDocsRestrictions, DocBookRenderer): 187 # TODO keep optionsDocBook diff small. remove soon if rendering is still good. 188 def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 189 token.meta['compact'] = False 190 return super().ordered_list_open(token, tokens, i) 191 def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 192 token.meta['compact'] = False 193 return super().bullet_list_open(token, tokens, i) 194 195class DocBookConverter(BaseConverter[OptionsDocBookRenderer]): 196 __option_block_separator__ = "" 197 198 def __init__(self, manpage_urls: Mapping[str, str], 199 revision: str, 200 document_type: str, 201 varlist_id: str, 202 id_prefix: str): 203 super().__init__(revision) 204 self._renderer = OptionsDocBookRenderer(manpage_urls) 205 self._document_type = document_type 206 self._varlist_id = varlist_id 207 self._id_prefix = id_prefix 208 209 def _parallel_render_prepare(self) -> Any: 210 return (self._renderer._manpage_urls, self._revision, self._document_type, 211 self._varlist_id, self._id_prefix) 212 @classmethod 213 def _parallel_render_init_worker(cls, a: Any) -> DocBookConverter: 214 return cls(*a) 215 216 def _related_packages_header(self) -> list[str]: 217 return [ 218 "<para>", 219 " <emphasis>Related packages:</emphasis>", 220 "</para>", 221 ] 222 223 def _decl_def_header(self, header: str) -> list[str]: 224 return [ 225 f"<para><emphasis>{header}:</emphasis></para>", 226 "<simplelist>" 227 ] 228 229 def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: 230 if href is not None: 231 href = " xlink:href=" + xml.quoteattr(href) 232 return [ 233 f"<member><filename{href}>", 234 xml.escape(name), 235 "</filename></member>" 236 ] 237 238 def _decl_def_footer(self) -> list[str]: 239 return [ "</simplelist>" ] 240 241 def finalize(self, *, fragment: bool = False) -> str: 242 result = [] 243 244 if not fragment: 245 result.append('<?xml version="1.0" encoding="UTF-8"?>') 246 if self._document_type == 'appendix': 247 result += [ 248 '<appendix xmlns="http://docbook.org/ns/docbook"', 249 ' xml:id="appendix-configuration-options">', 250 ' <title>Configuration Options</title>', 251 ] 252 result += [ 253 '<variablelist xmlns:xlink="http://www.w3.org/1999/xlink"', 254 ' xmlns:nixos="tag:nixos.org"', 255 ' xmlns="http://docbook.org/ns/docbook"', 256 f' xml:id="{self._varlist_id}">', 257 ] 258 259 for (name, opt) in self._sorted_options(): 260 id = make_xml_id(self._id_prefix + name) 261 result += [ 262 "<varlistentry>", 263 # NOTE adding extra spaces here introduces spaces into xref link expansions 264 (f"<term xlink:href={xml.quoteattr('#' + id)} xml:id={xml.quoteattr(id)}>" + 265 f"<option>{xml.escape(name)}</option></term>"), 266 "<listitem>" 267 ] 268 result += opt.lines 269 result += [ 270 "</listitem>", 271 "</varlistentry>" 272 ] 273 274 result.append("</variablelist>") 275 if self._document_type == 'appendix': 276 result.append("</appendix>") 277 278 return "\n".join(result) 279 280class OptionsManpageRenderer(OptionDocsRestrictions, ManpageRenderer): 281 pass 282 283class ManpageConverter(BaseConverter[OptionsManpageRenderer]): 284 __option_block_separator__ = ".sp" 285 286 _options_by_id: dict[str, str] 287 _links_in_last_description: Optional[list[str]] = None 288 289 def __init__(self, revision: str, 290 *, 291 # only for parallel rendering 292 _options_by_id: Optional[dict[str, str]] = None): 293 super().__init__(revision) 294 self._options_by_id = _options_by_id or {} 295 self._renderer = OptionsManpageRenderer({}, self._options_by_id) 296 297 def _parallel_render_prepare(self) -> Any: 298 return (self._revision, { '_options_by_id': self._options_by_id }) 299 @classmethod 300 def _parallel_render_init_worker(cls, a: Any) -> ManpageConverter: 301 return cls(a[0], **a[1]) 302 303 def _render_option(self, name: str, option: dict[str, Any]) -> RenderedOption: 304 links = self._renderer.link_footnotes = [] 305 result = super()._render_option(name, option) 306 self._renderer.link_footnotes = None 307 return result._replace(links=links) 308 309 def add_options(self, options: dict[str, Any]) -> None: 310 for (k, v) in options.items(): 311 self._options_by_id[f'#{make_xml_id(f"opt-{k}")}'] = k 312 return super().add_options(options) 313 314 def _render_code(self, option: dict[str, Any], key: str) -> list[str]: 315 try: 316 self._renderer.inline_code_is_quoted = False 317 return super()._render_code(option, key) 318 finally: 319 self._renderer.inline_code_is_quoted = True 320 321 def _related_packages_header(self) -> list[str]: 322 return [ 323 '\\fIRelated packages:\\fP', 324 '.sp', 325 ] 326 327 def _decl_def_header(self, header: str) -> list[str]: 328 return [ 329 f'\\fI{man_escape(header)}:\\fP', 330 ] 331 332 def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: 333 return [ 334 '.RS 4', 335 f'\\fB{man_escape(name)}\\fP', 336 '.RE' 337 ] 338 339 def _decl_def_footer(self) -> list[str]: 340 return [] 341 342 def finalize(self) -> str: 343 result = [] 344 345 result += [ 346 r'''.TH "CONFIGURATION\&.NIX" "5" "01/01/1980" "NixOS" "NixOS Reference Pages"''', 347 r'''.\" disable hyphenation''', 348 r'''.nh''', 349 r'''.\" disable justification (adjust text to left margin only)''', 350 r'''.ad l''', 351 r'''.\" enable line breaks after slashes''', 352 r'''.cflags 4 /''', 353 r'''.SH "NAME"''', 354 self._render('{file}`configuration.nix` - NixOS system configuration specification'), 355 r'''.SH "DESCRIPTION"''', 356 r'''.PP''', 357 self._render('The file {file}`/etc/nixos/configuration.nix` contains the ' 358 'declarative specification of your NixOS system configuration. ' 359 'The command {command}`nixos-rebuild` takes this file and ' 360 'realises the system configuration specified therein.'), 361 r'''.SH "OPTIONS"''', 362 r'''.PP''', 363 self._render('You can use the following options in {file}`configuration.nix`.'), 364 ] 365 366 for (name, opt) in self._sorted_options(): 367 result += [ 368 ".PP", 369 f"\\fB{man_escape(name)}\\fR", 370 ".RS 4", 371 ] 372 result += opt.lines 373 if links := opt.links: 374 result.append(self.__option_block_separator__) 375 md_links = "" 376 for i in range(0, len(links)): 377 md_links += "\n" if i > 0 else "" 378 if links[i].startswith('#opt-'): 379 md_links += f"{i+1}. see the {{option}}`{self._options_by_id[links[i]]}` option" 380 else: 381 md_links += f"{i+1}. " + md_escape(links[i]) 382 result.append(self._render(md_links)) 383 384 result.append(".RE") 385 386 result += [ 387 r'''.SH "AUTHORS"''', 388 r'''.PP''', 389 r'''Eelco Dolstra and the Nixpkgs/NixOS contributors''', 390 ] 391 392 return "\n".join(result) 393 394class OptionsCommonMarkRenderer(OptionDocsRestrictions, CommonMarkRenderer): 395 pass 396 397class CommonMarkConverter(BaseConverter[OptionsCommonMarkRenderer]): 398 __option_block_separator__ = "" 399 400 def __init__(self, manpage_urls: Mapping[str, str], revision: str): 401 super().__init__(revision) 402 self._renderer = OptionsCommonMarkRenderer(manpage_urls) 403 404 def _parallel_render_prepare(self) -> Any: 405 return (self._renderer._manpage_urls, self._revision) 406 @classmethod 407 def _parallel_render_init_worker(cls, a: Any) -> CommonMarkConverter: 408 return cls(*a) 409 410 def _related_packages_header(self) -> list[str]: 411 return [ "*Related packages:*" ] 412 413 def _decl_def_header(self, header: str) -> list[str]: 414 return [ f"*{header}:*" ] 415 416 def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: 417 if href is not None: 418 return [ f" - [{md_escape(name)}]({href})" ] 419 return [ f" - {md_escape(name)}" ] 420 421 def _decl_def_footer(self) -> list[str]: 422 return [] 423 424 def finalize(self) -> str: 425 result = [] 426 427 for (name, opt) in self._sorted_options(): 428 result.append(f"## {md_escape(name)}\n") 429 result += opt.lines 430 result.append("\n\n") 431 432 return "\n".join(result) 433 434class OptionsAsciiDocRenderer(OptionDocsRestrictions, AsciiDocRenderer): 435 pass 436 437class AsciiDocConverter(BaseConverter[OptionsAsciiDocRenderer]): 438 __option_block_separator__ = "" 439 440 def __init__(self, manpage_urls: Mapping[str, str], revision: str): 441 super().__init__(revision) 442 self._renderer = OptionsAsciiDocRenderer(manpage_urls) 443 444 def _parallel_render_prepare(self) -> Any: 445 return (self._renderer._manpage_urls, self._revision) 446 @classmethod 447 def _parallel_render_init_worker(cls, a: Any) -> AsciiDocConverter: 448 return cls(*a) 449 450 def _related_packages_header(self) -> list[str]: 451 return [ "__Related packages:__" ] 452 453 def _decl_def_header(self, header: str) -> list[str]: 454 return [ f"__{header}:__\n" ] 455 456 def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: 457 if href is not None: 458 return [ f"* link:{quote(href, safe='/:')}[{asciidoc_escape(name)}]" ] 459 return [ f"* {asciidoc_escape(name)}" ] 460 461 def _decl_def_footer(self) -> list[str]: 462 return [] 463 464 def finalize(self) -> str: 465 result = [] 466 467 for (name, opt) in self._sorted_options(): 468 result.append(f"== {asciidoc_escape(name)}\n") 469 result += opt.lines 470 result.append("\n\n") 471 472 return "\n".join(result) 473 474class OptionsHTMLRenderer(OptionDocsRestrictions, HTMLRenderer): 475 # TODO docbook compat. must be removed together with the matching docbook handlers. 476 def ordered_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 477 token.meta['compact'] = False 478 return super().ordered_list_open(token, tokens, i) 479 def bullet_list_open(self, token: Token, tokens: Sequence[Token], i: int) -> str: 480 token.meta['compact'] = False 481 return super().bullet_list_open(token, tokens, i) 482 def fence(self, token: Token, tokens: Sequence[Token], i: int) -> str: 483 # TODO use token.info. docbook doesn't so we can't yet. 484 return f'<pre class="programlisting">{html.escape(token.content)}</pre>' 485 486class HTMLConverter(BaseConverter[OptionsHTMLRenderer]): 487 __option_block_separator__ = "" 488 489 def __init__(self, manpage_urls: Mapping[str, str], revision: str, 490 varlist_id: str, id_prefix: str, xref_targets: Mapping[str, XrefTarget]): 491 super().__init__(revision) 492 self._xref_targets = xref_targets 493 self._varlist_id = varlist_id 494 self._id_prefix = id_prefix 495 self._renderer = OptionsHTMLRenderer(manpage_urls, self._xref_targets) 496 497 def _parallel_render_prepare(self) -> Any: 498 return (self._renderer._manpage_urls, self._revision, 499 self._varlist_id, self._id_prefix, self._xref_targets) 500 @classmethod 501 def _parallel_render_init_worker(cls, a: Any) -> HTMLConverter: 502 return cls(*a) 503 504 def _related_packages_header(self) -> list[str]: 505 return [ 506 '<p><span class="emphasis"><em>Related packages:</em></span></p>', 507 ] 508 509 def _decl_def_header(self, header: str) -> list[str]: 510 return [ 511 f'<p><span class="emphasis"><em>{header}:</em></span></p>', 512 '<table border="0" summary="Simple list" class="simplelist">' 513 ] 514 515 def _decl_def_entry(self, href: Optional[str], name: str) -> list[str]: 516 if href is not None: 517 href = f' href="{html.escape(href, True)}"' 518 return [ 519 "<tr><td>", 520 f'<code class="filename"><a class="filename" {href} target="_top">', 521 f'{html.escape(name)}', 522 '</a></code>', 523 "</td></tr>" 524 ] 525 526 def _decl_def_footer(self) -> list[str]: 527 return [ "</table>" ] 528 529 def finalize(self) -> str: 530 result = [] 531 532 result += [ 533 '<div class="variablelist">', 534 f'<a id="{html.escape(self._varlist_id, True)}"></a>', 535 ' <dl class="variablelist">', 536 ] 537 538 for (name, opt) in self._sorted_options(): 539 id = make_xml_id(self._id_prefix + name) 540 target = self._xref_targets[id] 541 result += [ 542 '<dt>', 543 ' <span class="term">', 544 # docbook compat, these could be one tag 545 f' <a id="{html.escape(id, True)}"></a><a class="term" href="{target.href()}">' 546 # no spaces here (and string merging) for docbook output compat 547 f'<code class="option">{html.escape(name)}</code>', 548 ' </a>', 549 ' </span>', 550 '</dt>', 551 '<dd>', 552 ] 553 result += opt.lines 554 result += [ 555 "</dd>", 556 ] 557 558 result += [ 559 " </dl>", 560 "</div>" 561 ] 562 563 return "\n".join(result) 564 565def _build_cli_db(p: argparse.ArgumentParser) -> None: 566 p.add_argument('--manpage-urls', required=True) 567 p.add_argument('--revision', required=True) 568 p.add_argument('--document-type', required=True) 569 p.add_argument('--varlist-id', required=True) 570 p.add_argument('--id-prefix', required=True) 571 p.add_argument("infile") 572 p.add_argument("outfile") 573 574def _build_cli_manpage(p: argparse.ArgumentParser) -> None: 575 p.add_argument('--revision', required=True) 576 p.add_argument("infile") 577 p.add_argument("outfile") 578 579def _build_cli_commonmark(p: argparse.ArgumentParser) -> None: 580 p.add_argument('--manpage-urls', required=True) 581 p.add_argument('--revision', required=True) 582 p.add_argument("infile") 583 p.add_argument("outfile") 584 585def _build_cli_asciidoc(p: argparse.ArgumentParser) -> None: 586 p.add_argument('--manpage-urls', required=True) 587 p.add_argument('--revision', required=True) 588 p.add_argument("infile") 589 p.add_argument("outfile") 590 591def _run_cli_db(args: argparse.Namespace) -> None: 592 with open(args.manpage_urls, 'r') as manpage_urls: 593 md = DocBookConverter( 594 json.load(manpage_urls), 595 revision = args.revision, 596 document_type = args.document_type, 597 varlist_id = args.varlist_id, 598 id_prefix = args.id_prefix) 599 600 with open(args.infile, 'r') as f: 601 md.add_options(json.load(f)) 602 with open(args.outfile, 'w') as f: 603 f.write(md.finalize()) 604 605def _run_cli_manpage(args: argparse.Namespace) -> None: 606 md = ManpageConverter(revision = args.revision) 607 608 with open(args.infile, 'r') as f: 609 md.add_options(json.load(f)) 610 with open(args.outfile, 'w') as f: 611 f.write(md.finalize()) 612 613def _run_cli_commonmark(args: argparse.Namespace) -> None: 614 with open(args.manpage_urls, 'r') as manpage_urls: 615 md = CommonMarkConverter(json.load(manpage_urls), revision = args.revision) 616 617 with open(args.infile, 'r') as f: 618 md.add_options(json.load(f)) 619 with open(args.outfile, 'w') as f: 620 f.write(md.finalize()) 621 622def _run_cli_asciidoc(args: argparse.Namespace) -> None: 623 with open(args.manpage_urls, 'r') as manpage_urls: 624 md = AsciiDocConverter(json.load(manpage_urls), revision = args.revision) 625 626 with open(args.infile, 'r') as f: 627 md.add_options(json.load(f)) 628 with open(args.outfile, 'w') as f: 629 f.write(md.finalize()) 630 631def build_cli(p: argparse.ArgumentParser) -> None: 632 formats = p.add_subparsers(dest='format', required=True) 633 _build_cli_db(formats.add_parser('docbook')) 634 _build_cli_manpage(formats.add_parser('manpage')) 635 _build_cli_commonmark(formats.add_parser('commonmark')) 636 _build_cli_asciidoc(formats.add_parser('asciidoc')) 637 638def run_cli(args: argparse.Namespace) -> None: 639 if args.format == 'docbook': 640 _run_cli_db(args) 641 elif args.format == 'manpage': 642 _run_cli_manpage(args) 643 elif args.format == 'commonmark': 644 _run_cli_commonmark(args) 645 elif args.format == 'asciidoc': 646 _run_cli_asciidoc(args) 647 else: 648 raise RuntimeError('format not hooked up', args)