1# -*- coding: utf-8 -*- # 2# Copyright 2015 Google LLC. All Rights Reserved. 3# 4# Licensed under the Apache License, Version 2.0 (the "License"); 5# you may not use this file except in compliance with the License. 6# You may obtain a copy of the License at 7# 8# http://www.apache.org/licenses/LICENSE-2.0 9# 10# Unless required by applicable law or agreed to in writing, software 11# distributed under the License is distributed on an "AS IS" BASIS, 12# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13# See the License for the specific language governing permissions and 14# limitations under the License. 15 16"""Cloud SDK markdown document renderer. 17 18This module marshals markdown renderers to convert Cloud SDK markdown to text, 19HTML and manpage documents. The renderers are self-contained, allowing the 20Cloud SDK runtime to generate documents on the fly for all target architectures. 21 22The MarkdownRenderer class parses markdown from an input stream and renders it 23using the Renderer class. The Renderer member functions provide an abstract 24document model that matches markdown entities to the output document, e.g., font 25embellishment, section headings, lists, hanging indents, text margins, tables. 26There is a Renderer derived class for each output style that writes the result 27on an output stream returns Rendere.Finish(). 28""" 29 30from __future__ import absolute_import 31from __future__ import division 32from __future__ import unicode_literals 33 34import argparse 35import re 36import sys 37 38from googlecloudsdk.core import argv_utils 39from googlecloudsdk.core import exceptions 40from googlecloudsdk.core.document_renderers import devsite_renderer 41from googlecloudsdk.core.document_renderers import html_renderer 42from googlecloudsdk.core.document_renderers import linter_renderer 43from googlecloudsdk.core.document_renderers import man_renderer 44from googlecloudsdk.core.document_renderers import markdown_renderer 45from googlecloudsdk.core.document_renderers import renderer 46from googlecloudsdk.core.document_renderers import text_renderer 47 48 49STYLES = { 50 'devsite': devsite_renderer.DevSiteRenderer, 51 'html': html_renderer.HTMLRenderer, 52 'man': man_renderer.ManRenderer, 53 'markdown': markdown_renderer.MarkdownRenderer, 54 'text': text_renderer.TextRenderer, 55 'linter': linter_renderer.LinterRenderer 56} 57 58 59def _GetNestedGroup(buf, i, beg, end): 60 """Returns the index in buf of the end of the nested beg...end group. 61 62 Args: 63 buf: Input buffer. 64 i: The buf[] index of the first beg character. 65 beg: The group begin character. 66 end: The group end character. 67 68 Returns: 69 The index in buf of the end of the nested beg...end group, 0 if there is 70 no group. 71 """ 72 if buf[i] != beg: 73 return 0 74 nesting = 0 75 while i < len(buf): 76 if buf[i] == beg: 77 nesting += 1 78 elif buf[i] == end: 79 nesting -= 1 80 if nesting <= 0: 81 return i 82 i += 1 83 return 0 84 85 86def _IsValidTarget(target): 87 """Returns True if target is a valid anchor/link target.""" 88 return not any(c in target for c in ' ,()[]') 89 90 91class DocumentStyleError(exceptions.Error): 92 """An exception for unknown document styles.""" 93 94 def __init__(self, style): 95 message = ('Unknown markdown document style [{style}] -- must be one of:' 96 ' {styles}.'.format(style=style, 97 styles=', '.join(sorted(STYLES.keys())))) 98 super(DocumentStyleError, self).__init__(message) 99 100 101class _ListElementState(object): 102 """List element state. 103 104 Attributes: 105 bullet: True if the current element is a bullet. 106 ignore_line: The number of blank line requests to ignore. 107 level: List element nesting level counting from 0. 108 """ 109 110 def __init__(self): 111 self.bullet = False 112 self.ignore_line = 0 113 self.level = 0 114 115 116class MarkdownRenderer(object): 117 """Reads markdown and renders to a document. 118 119 Attributes: 120 _EMPHASIS: The font emphasis attribute dict indexed by markdown character. 121 _buf: The current output line. 122 _code_block_indent: ```...``` code block indent if >= 0. 123 _depth: List nesting depth counting from 0. 124 _edit: True if NOTES edits are required. 125 _example: The current example indentation space count. 126 _fin: The markdown input stream. 127 _line: The current input line. 128 _lists: _ListElementState list element state stack indexed by _depth. 129 _next_example: The next example indentation space count. 130 _notes: Additional text for the NOTES section. 131 _paragraph: True if the last line was ``+'' paragraph at current indent. 132 _next_paragraph: The next line starts a new paragraph at same indentation. 133 _renderer: The document_renderer.Renderer subclass. 134 command_metadata: Optional metadata of command. 135 """ 136 _EMPHASIS = {'*': renderer.BOLD, '_': renderer.ITALIC, '`': renderer.CODE} 137 138 def __init__(self, style_renderer, fin=sys.stdin, notes=None, 139 command_metadata=None): 140 """Initializes the renderer. 141 142 Args: 143 style_renderer: The document_renderer.Renderer subclass. 144 fin: The markdown input stream. 145 notes: Optional sentences for the NOTES section. 146 command_metadata: Optional metadata of command. 147 """ 148 self._renderer = style_renderer 149 self._buf = '' 150 self._fin = fin 151 self._notes = notes 152 self._edit = self._notes 153 self._lists = [_ListElementState()] 154 self._code_block_indent = -1 155 self._depth = 0 156 self._example = 0 157 self._next_example = 0 158 self._paragraph = False 159 self._peek = None 160 self._next_paragraph = False 161 self._line = None 162 self.command_metadata = command_metadata 163 self._example_regex = '$ gcloud' 164 self._last_list_level = None 165 166 def _AnchorStyle1(self, buf, i): 167 """Checks for link:target[text] hyperlink anchor markdown. 168 169 Hyperlink anchors are of the form: 170 <link> ':' <target> [ '[' <text> ']' ] 171 For example: 172 http://www.google.com[Google Search] 173 The underlying renderer determines how the parts are displayed. 174 175 Args: 176 buf: Input buffer. 177 i: The buf[] index of ':'. 178 179 Returns: 180 (i, back, target, text) 181 i: The buf[] index just past the link, 0 if no link. 182 back: The number of characters to retain before buf[i]. 183 target: The link target. 184 text: The link text. 185 """ 186 if i >= 3 and buf[i - 3:i] == 'ftp': 187 back = 3 188 target_beg = i - 3 189 elif i >= 4 and buf[i - 4:i] == 'http': 190 back = 4 191 target_beg = i - 4 192 elif i >= 4 and buf[i - 4:i] == 'link': 193 back = 4 194 target_beg = i + 1 195 elif i >= 5 and buf[i - 5:i] == 'https': 196 back = 5 197 target_beg = i - 5 198 elif i >= 6 and buf[i - 6:i] == 'mailto': 199 back = 6 200 target_beg = i - 6 201 else: 202 return 0, 0, None, None 203 text_beg = 0 204 text_end = 0 205 while True: 206 if i >= len(buf) or buf[i].isspace(): 207 # Just a link with no text. 208 if buf[i - 1] == '.': 209 # Drop trailing '.' that is probably a sentence-ending period. 210 i -= 1 211 target_end = i 212 text_beg = i 213 text_end = i - 1 214 break 215 if buf[i] == '[': 216 # Explicit link text inside [...]. 217 target_end = i 218 text_beg = i + 1 219 text_end = _GetNestedGroup(buf, i, '[', ']') 220 break 221 if buf[i] in '{}()<>\'"`*': 222 # Reject code sample or parameterized links 223 break 224 i += 1 225 if not text_end: 226 return 0, 0, None, None 227 return (text_end + 1, back, buf[target_beg:target_end], 228 buf[text_beg:text_end]) 229 230 def _AnchorStyle2(self, buf, i): 231 """Checks for [text](target) hyperlink anchor markdown. 232 233 Hyperlink anchors are of the form: 234 '[' <text> ']' '(' <target> ')' 235 For example: 236 [Google Search](http://www.google.com) 237 [](http://www.show.the.link) 238 The underlying renderer determines how the parts are displayed. 239 240 Args: 241 buf: Input buffer. 242 i: The buf[] index of ':'. 243 244 Returns: 245 (i, target, text) 246 i: The buf[] index just past the link, 0 if no link. 247 target: The link target. 248 text: The link text. 249 """ 250 text_beg = i + 1 251 text_end = _GetNestedGroup(buf, i, '[', ']') 252 if not text_end or text_end >= len(buf) - 1 or buf[text_end + 1] != '(': 253 return 0, None, None 254 target_beg = text_end + 2 255 target_end = _GetNestedGroup(buf, target_beg - 1, '(', ')') 256 if not target_end or target_end <= target_beg: 257 return 0, None, None 258 return (target_end + 1, buf[target_beg:target_end], buf[text_beg:text_end]) 259 260 def _Attributes(self, buf=None): 261 """Converts inline markdown attributes in self._buf. 262 263 Args: 264 buf: Convert markdown from this string instead of self._buf. 265 266 Returns: 267 A string with markdown attributes converted to render properly. 268 """ 269 # String append used on ret below because of anchor text look behind. 270 emphasis = '' if self._code_block_indent >= 0 or self._example else '*_`' 271 ret = '' 272 if buf is None: 273 buf = self._buf 274 self._buf = '' 275 if buf: 276 buf = self._renderer.Escape(buf) 277 i = 0 278 is_literal = False 279 while i < len(buf): 280 c = buf[i] 281 if c == ':': 282 index_after_anchor, back, target, text = self._AnchorStyle1(buf, i) 283 if index_after_anchor and _IsValidTarget(target): 284 ret = ret[:-back] 285 i = index_after_anchor - 1 286 c = self._renderer.Link(target, text) 287 elif c == '[': 288 index_after_anchor, target, text = self._AnchorStyle2(buf, i) 289 if index_after_anchor and _IsValidTarget(target): 290 i = index_after_anchor - 1 291 c = self._renderer.Link(target, text) 292 elif c in emphasis: 293 # Treating some apparent font embelishment markdown as literal input 294 # is the hairiest part of markdown. This code catches the common 295 # programming clash of '*' as a literal globbing character in path 296 # matching examples. It basically works for the current use cases. 297 l = buf[i - 1] if i else ' ' # The char before c. 298 r = buf[i + 1] if i < len(buf) - 1 else ' ' # The char after c. 299 if l != '`' and c == '`' and r == '`': 300 x = buf[i + 2] if i < len(buf) - 2 else ' ' # The char after r. 301 if x == '`': 302 # Render inline ```...``` code block enclosed literals. 303 index_at_code_block_quote = buf.find('```', i + 2) 304 if index_at_code_block_quote > 0: 305 ret += self._renderer.Font(renderer.CODE) 306 ret += buf[i + 3:index_at_code_block_quote] 307 ret += self._renderer.Font(renderer.CODE) 308 i = index_at_code_block_quote + 3 309 continue 310 else: 311 # Render inline air quotes along with the enclosed literals. 312 index_at_air_quote = buf.find("''", i) 313 if index_at_air_quote > 0: 314 index_at_air_quote += 2 315 ret += buf[i:index_at_air_quote] 316 i = index_at_air_quote 317 continue 318 if r == c: 319 # Doubled markers are literal. 320 c += c 321 i += 1 322 elif (c == '*' and l in ' /' and r in ' ./' or 323 c != '`' and l in ' /' and r in ' .'): 324 # Path-like glob patterns are literal. 325 pass 326 elif l.isalnum() and r.isalnum(): 327 # No embellishment switching in words. 328 pass 329 elif is_literal and c == '*': 330 # '*' should be considered as literal when contained in code block 331 pass 332 else: 333 if c == '`': 334 # mark code block start or end 335 is_literal = not is_literal 336 c = self._renderer.Font(self._EMPHASIS[c]) 337 ret += c 338 i += 1 339 return self._renderer.Entities(ret) 340 341 def _Example(self, i): 342 """Renders self._line[i:] as an example. 343 344 This is a helper function for _ConvertCodeBlock() and _ConvertExample(). 345 346 Args: 347 i: The current character index in self._line. 348 """ 349 if self._line[i:]: 350 self._Fill() 351 if not self._example or self._example > i: 352 self._example = i 353 self._next_example = self._example 354 self._buf = self._line[self._example:] 355 self._renderer.Example(self._Attributes()) 356 357 def _Fill(self): 358 """Sends self._buf to the renderer and clears self._buf.""" 359 if self._buf: 360 self._renderer.Fill(self._Attributes()) 361 362 def _ReadLine(self): 363 """Reads and possibly preprocesses the next markdown line from self._fin. 364 365 Returns: 366 The next markdown input line. 367 """ 368 if self._peek is not None: 369 line = self._peek 370 self._peek = None 371 return line 372 return self._fin.readline() 373 374 def _PushBackLine(self, line): 375 """Pushes back one lookahead line. The next _ReadlLine will return line.""" 376 self._peek = line 377 378 def _ConvertMarkdownToMarkdown(self): 379 """Generates markdown with additonal NOTES if requested.""" 380 if not self._edit: 381 self._renderer.Write(self._fin.read()) 382 return 383 while True: 384 line = self._ReadLine() 385 if not line: 386 break 387 self._renderer.Write(line) 388 if self._notes and line == '## NOTES\n': 389 self._renderer.Write('\n' + self._notes + '\n') 390 self._notes = '' 391 if self._notes: 392 self._renderer.Write('\n\n## NOTES\n\n' + self._notes + '\n') 393 394 def _ConvertBlankLine(self, i): 395 """Detects and converts a blank markdown line (length 0). 396 397 Resets the indentation to the default and emits a blank line. Multiple 398 blank lines are suppressed in the output. 399 400 Args: 401 i: The current character index in self._line. 402 403 Returns: 404 -1 if the input line is a blank markdown, i otherwise. 405 """ 406 if self._line: 407 return i 408 self._Fill() 409 if self._lists[self._depth].bullet: 410 self._renderer.List(0) 411 if self._depth: 412 self._depth -= 1 413 else: 414 self._lists[self._depth].bullet = False 415 if self._lists[self._depth].ignore_line: 416 self._lists[self._depth].ignore_line -= 1 417 if not self._depth or not self._lists[self._depth].ignore_line: 418 self._renderer.Line() 419 return -1 420 421 def _ConvertParagraph(self, i): 422 """Detects and converts + markdown line (length 1). 423 424 Emits a blank line but retains the current indent. 425 426 Args: 427 i: The current character index in self._line. 428 429 Returns: 430 -1 if the input line is a '+' markdown, i otherwise. 431 """ 432 if len(self._line) != 1 or self._line[0] != '+': 433 return i 434 self._Fill() 435 self._renderer.Line() 436 self._next_paragraph = True 437 return -1 438 439 def _ConvertHeading(self, i): 440 """Detects and converts a markdown heading line. 441 442 = level-1 [=] 443 # level-1 [#] 444 == level-2 [==] 445 ## level-2 [##] 446 447 Args: 448 i: The current character index in self._line. 449 450 Returns: 451 -1 if the input line is a heading markdown, i otherwise. 452 """ 453 start_index = i 454 marker = self._line[i] 455 if marker not in ('=', '#'): 456 return start_index 457 while i < len(self._line) and self._line[i] == marker: 458 i += 1 459 if i >= len(self._line) or self._line[i] != ' ': 460 return start_index 461 if self._line[-1] == marker: 462 if (not self._line.endswith(self._line[start_index:i]) or 463 self._line[-(i - start_index + 1)] != ' '): 464 return start_index 465 end_index = -(i - start_index + 1) 466 else: 467 end_index = len(self._line) 468 self._Fill() 469 self._buf = self._line[i + 1:end_index] 470 heading = self._Attributes() 471 if i == 1 and heading.endswith('(1)'): 472 self._renderer.SetCommand(heading[:-3].lower().split('_')) 473 self._renderer.Heading(i, heading) 474 self._depth = 0 475 if heading in ['NAME', 'SYNOPSIS']: 476 if heading == 'SYNOPSIS': 477 is_synopsis_section = True 478 else: 479 is_synopsis_section = False 480 while True: 481 self._buf = self._ReadLine() 482 if not self._buf: 483 break 484 self._buf = self._buf.rstrip() 485 if self._buf: 486 self._renderer.Synopsis(self._Attributes(), 487 is_synopsis=is_synopsis_section) 488 break 489 elif self._notes and heading == 'NOTES': 490 self._buf = self._notes 491 self._notes = None 492 return -1 493 494 def _ConvertOldTable(self, i): 495 """Detects and converts a sequence of markdown table lines. 496 497 This method will consume multiple input lines if the current line is a 498 table heading. The table markdown sequence is: 499 500 [...format="csv"...] 501 |====* 502 col-1-data-item,col-2-data-item... 503 ... 504 <blank line ends table> 505 506 Args: 507 i: The current character index in self._line. 508 509 Returns: 510 -1 if the input lines are table markdown, i otherwise. 511 """ 512 if (self._line[0] != '[' or self._line[-1] != ']' or 513 'format="csv"' not in self._line): 514 return i 515 line = self._ReadLine() 516 if not line: 517 return i 518 if not line.startswith('|===='): 519 self._PushBackLine(line) 520 return i 521 522 rows = [] 523 while True: 524 self._buf = self._ReadLine() 525 if not self._buf: 526 break 527 self._buf = self._buf.rstrip() 528 if self._buf.startswith('|===='): 529 break 530 rows.append(self._Attributes().split(',')) 531 self._buf = '' 532 533 table = renderer.TableAttributes() 534 if len(rows) > 1: 535 for label in rows[0]: 536 table.AddColumn(label=label) 537 rows = rows[1:] 538 if table.columns and rows: 539 self._renderer.Table(table, rows) 540 return -1 541 542 def _ConvertTable(self, i): 543 """Detects and converts a sequence of markdown table lines. 544 545 Markdown attributes are not supported in headings or column data. 546 547 This method will consume multiple input lines if the current line is a 548 table heading or separator line. The table markdown sequence is: 549 550 heading line 551 552 heading-1 | ... | heading-n 553 OR for boxed table 554 | heading-1 | ... | heading-n | 555 556 separator line 557 558 --- | ... | --- 559 OR for boxed table 560 | --- | ... | --- | 561 WHERE 562 :--- align left 563 :---: align center 564 ---: align right 565 ----* length >= fixed_width_length sets column fixed width 566 567 row data lines 568 569 col-1-data-item | ... | col-n-data-item 570 ... 571 572 blank line ends table 573 574 Args: 575 i: The current character index in self._line. 576 577 Returns: 578 -1 if the input lines are table markdown, i otherwise. 579 """ 580 fixed_width_length = 8 581 582 if ' | ' not in self._line: 583 return self._ConvertOldTable(i) 584 if '---' in self._line: 585 head = False 586 line = self._line 587 else: 588 head = True 589 line = self._ReadLine() 590 if not line or '---' not in line: 591 if line is not self._line: 592 self._PushBackLine(line) 593 return self._ConvertOldTable(i) 594 595 # Parse the heading and separator lines. 596 597 box = False 598 if head: 599 heading = re.split(r' *\| *', self._line.strip()) 600 if not heading[0] and not heading[-1]: 601 heading = heading[1:-1] 602 box = True 603 else: 604 heading = [] 605 sep = re.split(r' *\| *', line.strip()) 606 if not sep[0] and not sep[-1]: 607 sep = sep[1:-1] 608 box = True 609 if heading and len(heading) != len(sep): 610 if line is not self._line: 611 self._PushBackLine(line) 612 return self._ConvertOldTable(i) 613 614 # Committed to table markdown now. 615 616 table = renderer.TableAttributes(box=box) 617 618 # Determine the column attributes. 619 620 for index in range(len(sep)): 621 align = 'left' 622 s = sep[index] 623 if s.startswith(':'): 624 if s.endswith(':'): 625 align = 'center' 626 elif s.endswith(':'): 627 align = 'right' 628 label = heading[index] if index < len(heading) else None 629 width = len(s) if len(s) >= fixed_width_length else 0 630 table.AddColumn(align=align, label=label, width=width) 631 632 # Collect the column data by rows. Blank or + line terminates the data. 633 634 rows = [] 635 while True: 636 line = self._ReadLine() 637 if line in (None, '', '\n', '+\n'): 638 self._PushBackLine(line) 639 break 640 row = re.split(r' *\| *', line.rstrip()) 641 rows.append(row) 642 643 if rows: 644 self._renderer.Table(table, rows) 645 self._buf = '' 646 return -1 647 648 def _ConvertIndentation(self, i): 649 """Advances i past any indentation spaces. 650 651 Args: 652 i: The current character index in self._line. 653 654 Returns: 655 i after indentation spaces skipped. 656 """ 657 while i < len(self._line) and self._line[i] == ' ': 658 i += 1 659 return i 660 661 def _ConvertCodeBlock(self, i): 662 """Detects and converts a ```...``` code block markdown. 663 664 Args: 665 i: The current character index in self._line. 666 667 Returns: 668 -1 if the input line is part of a code block markdown, i otherwise. 669 """ 670 if self._line[i:].startswith('```'): 671 lang = self._line[i+3:] 672 if not lang: 673 if self._code_block_indent >= 0: 674 self._code_block_indent = -1 675 else: 676 self._code_block_indent = i 677 self._renderer.SetLang('' if self._code_block_indent >= 0 else None) 678 return -1 679 if self._code_block_indent < 0 and lang.isalnum(): 680 self._renderer.SetLang(lang) 681 self._code_block_indent = i 682 return -1 683 if self._code_block_indent < 0: 684 return i 685 self._Example(self._code_block_indent) 686 return -1 687 688 def _ConvertDefinitionList(self, i): 689 """Detects and converts a definition list item markdown line. 690 691 [item-level-1]:: [definition-line] 692 [definition-lines] 693 [item-level-2]::: [definition-line] 694 [definition-lines] 695 696 Args: 697 i: The current character index in self._line. 698 699 Returns: 700 -1 if the input line is a definition list item markdown, i otherwise. 701 """ 702 if i: 703 return i 704 index_at_definition_markdown = self._line.find('::') 705 if index_at_definition_markdown < 0: 706 return i 707 level = 1 708 list_level = None 709 i = index_at_definition_markdown + 2 710 while i < len(self._line) and self._line[i] == ':': 711 i += 1 712 level += 1 713 while i < len(self._line) and self._line[i].isspace(): 714 i += 1 715 end = i >= len(self._line) and not index_at_definition_markdown 716 if end: 717 # Bare ^:::$ is end of list which pops to previous list level. 718 level -= 1 719 if self._line.endswith('::'): 720 self._last_list_level = level 721 elif self._last_list_level and not self._line.startswith('::'): 722 list_level = self._last_list_level + 1 723 if (self._lists[self._depth].bullet or 724 self._lists[self._depth].level < level): 725 self._depth += 1 726 if self._depth >= len(self._lists): 727 self._lists.append(_ListElementState()) 728 else: 729 while self._lists[self._depth].level > level: 730 self._depth -= 1 731 self._Fill() 732 if end: 733 i = len(self._line) 734 definition = None 735 else: 736 self._lists[self._depth].bullet = False 737 self._lists[self._depth].ignore_line = 2 738 self._lists[self._depth].level = level 739 self._buf = self._line[:index_at_definition_markdown] 740 definition = self._Attributes() 741 if list_level: 742 level = list_level 743 self._renderer.List(level, definition=definition, end=end) 744 if i < len(self._line): 745 self._buf += self._line[i:] 746 return -1 747 748 def _ConvertBulletList(self, i): 749 """Detects and converts a bullet list item markdown line. 750 751 The list item indicator may be '-' or '*'. nesting by multiple indicators: 752 753 - level-1 754 -- level-2 755 - level-1 756 757 or nesting by indicator indentation: 758 759 * level-1 760 * level-2 761 * level-1 762 763 Args: 764 i: The current character index in self._line. 765 766 Returns: 767 -1 if the input line is a bullet list item markdown, i otherwise. 768 """ 769 if self._example or self._line[i] not in '-*': 770 return i 771 bullet = self._line[i] 772 level = i / 2 773 start_index = i 774 while i < len(self._line) and self._line[i] == bullet: 775 i += 1 776 level += 1 777 if i >= len(self._line) or self._line[i] != ' ': 778 return start_index 779 if (self._lists[self._depth].bullet and 780 self._lists[self._depth].level >= level): 781 while self._lists[self._depth].level > level: 782 self._depth -= 1 783 else: 784 self._depth += 1 785 if self._depth >= len(self._lists): 786 self._lists.append(_ListElementState()) 787 self._lists[self._depth].bullet = True 788 self._lists[self._depth].ignore_line = 0 789 self._lists[self._depth].level = level 790 self._Fill() 791 self._renderer.List(self._depth) 792 while i < len(self._line) and self._line[i] == ' ': 793 i += 1 794 self._buf += self._line[i:] 795 return -1 796 797 def _ConvertExample(self, i): 798 """Detects and converts an example markdown line. 799 800 Example lines are indented by one or more space characters. 801 802 Args: 803 i: The current character index in self._line. 804 805 Returns: 806 -1 if the input line is is an example line markdown, i otherwise. 807 """ 808 if not i or self._depth and not (self._example or self._paragraph): 809 return i 810 self._Example(i) 811 return -1 812 813 def _ConvertEndOfList(self, i): 814 """Detects and converts an end of list markdown line. 815 816 Args: 817 i: The current character index in self._line. 818 819 Returns: 820 -1 if the input line is an end of list markdown, i otherwise. 821 """ 822 if i or not self._depth: 823 return i 824 if self._lists[self._depth].ignore_line > 1: 825 self._lists[self._depth].ignore_line -= 1 826 if not self._lists[self._depth].ignore_line: 827 self._Fill() 828 self._renderer.List(0) 829 self._depth = 0 830 return i # More conversion possible. 831 832 def _ConvertRemainder(self, i): 833 """Detects and converts any remaining markdown text. 834 835 The input line is always consumed by this method. It should be the last 836 _Convert*() method called for each input line. 837 838 Args: 839 i: The current character index in self._line. 840 841 Returns: 842 -1 843 """ 844 self._buf += ' ' + self._line[i:] 845 return -1 846 847 def _Finish(self): 848 """Flushes the fill buffer and checks for NOTES. 849 850 A previous _ConvertHeading() will have cleared self._notes if a NOTES 851 section has already been seen. 852 853 Returns: 854 The renderer Finish() value. 855 """ 856 self._Fill() 857 if self._notes: 858 self._renderer.Line() 859 self._renderer.Heading(2, 'NOTES') 860 self._buf += self._notes 861 self._Fill() 862 return self._renderer.Finish() 863 864 def Run(self): 865 """Renders the markdown from fin to out and returns renderer.Finish().""" 866 if isinstance(self._renderer, markdown_renderer.MarkdownRenderer): 867 self._ConvertMarkdownToMarkdown() 868 return 869 while True: 870 self._example = self._next_example 871 self._next_example = 0 872 self._paragraph = self._next_paragraph 873 self._next_paragraph = False 874 self._line = self._ReadLine() 875 if not self._line: 876 break 877 if self._line.startswith(self._example_regex): 878 self._line = ' ' * self._example + ' ' + self._line 879 self._line = self._line.rstrip() 880 i = 0 881 # Each _Convert*() function can: 882 # - consume the markdown at self._line[i:] and return -1 883 # - ignore self._line[i:] and return i 884 # - change the class state, optionally advance i, and return i 885 # Conversion on the current state._line stop when -1 is returned. 886 for detect_and_convert in [ 887 self._ConvertBlankLine, 888 self._ConvertParagraph, 889 self._ConvertHeading, 890 self._ConvertTable, 891 self._ConvertIndentation, 892 self._ConvertCodeBlock, 893 self._ConvertDefinitionList, 894 self._ConvertBulletList, 895 self._ConvertExample, 896 self._ConvertEndOfList, 897 self._ConvertRemainder]: 898 i = detect_and_convert(i) 899 if i < 0: 900 break 901 return self._Finish() 902 903 904def RenderDocument(style='text', fin=None, out=None, width=80, notes=None, 905 title=None, command_metadata=None): 906 """Renders markdown to a selected document style. 907 908 Args: 909 style: The rendered document style name, must be one of the STYLES keys. 910 fin: The input stream containing the markdown. 911 out: The output stream for the rendered document. 912 width: The page width in characters. 913 notes: Optional sentences inserted in the NOTES section. 914 title: The document title. 915 command_metadata: Optional metadata of command, including available flags. 916 917 Raises: 918 DocumentStyleError: The markdown style was unknown. 919 """ 920 if style not in STYLES: 921 raise DocumentStyleError(style) 922 style_renderer = STYLES[style](out=out or sys.stdout, title=title, 923 width=width, command_metadata=command_metadata) 924 MarkdownRenderer(style_renderer, fin=fin or sys.stdin, notes=notes, 925 command_metadata=command_metadata).Run() 926 927 928class CommandMetaData(object): 929 """Object containing metadata of command to be passed into linter renderer.""" 930 931 def __init__(self, flags=None, bool_flags=None, is_group=True): 932 self.flags = flags if flags else [] 933 self.bool_flags = bool_flags if bool_flags else [] 934 self.is_group = is_group 935 936 937def main(argv): 938 """Standalone markdown document renderer.""" 939 940 parser = argparse.ArgumentParser( 941 description='Renders markdown on the standard input into a document on ' 942 'the standard output.') 943 944 parser.add_argument( 945 '--notes', 946 metavar='SENTENCES', 947 help='Inserts SENTENCES into the NOTES section which is created if ' 948 'needed.') 949 950 parser.add_argument( 951 '--style', 952 metavar='STYLE', 953 choices=sorted(STYLES.keys()), 954 default='text', 955 help='The output style.') 956 957 parser.add_argument( 958 '--title', 959 metavar='TITLE', 960 help='The document title.') 961 962 args = parser.parse_args(argv[1:]) 963 964 RenderDocument(args.style, notes=args.notes, title=args.title, 965 command_metadata=None) 966 967 968if __name__ == '__main__': 969 main(argv_utils.GetDecodedArgv()) 970