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