bb217012653f1cec1cccb8ee2d4fe86f09453f16
[platform/framework/web/chromium-efl.git] / third_party / catapult / devil / devil / utils / markdown.py
1 #! /usr/bin/env python
2 # Copyright 2016 The Chromium Authors. All rights reserved.
3 # Use of this source code is governed by a BSD-style license that can be
4 # found in the LICENSE file.
5
6 from __future__ import print_function
7
8 import argparse
9 import imp
10 import os
11 import re
12 import sys
13 import textwrap
14 import types
15
16 # A markdown code block template: https://goo.gl/9EsyRi
17 _CODE_BLOCK_FORMAT = '''```{language}
18 {code}
19 ```
20 '''
21
22 _DEVIL_ROOT = os.path.abspath(
23     os.path.join(os.path.dirname(__file__), '..', '..'))
24
25
26 def md_bold(raw_text):
27   """Returns markdown-formatted bold text."""
28   return '**%s**' % md_escape(raw_text, characters='*')
29
30
31 def md_code(raw_text, language):
32   """Returns a markdown-formatted code block in the given language."""
33   return _CODE_BLOCK_FORMAT.format(
34       language=language or '', code=md_escape(raw_text, characters='`'))
35
36
37 def md_escape(raw_text, characters='*_'):
38   """Escapes * and _."""
39
40   def escape_char(m):
41     return '\\%s' % m.group(0)
42
43   pattern = '[%s]' % re.escape(characters)
44   return re.sub(pattern, escape_char, raw_text)
45
46
47 def md_heading(raw_text, level):
48   """Returns markdown-formatted heading."""
49   adjusted_level = min(max(level, 0), 6)
50   return '%s%s%s' % ('#' * adjusted_level, ' ' if adjusted_level > 0 else '',
51                      raw_text)
52
53
54 def md_inline_code(raw_text):
55   """Returns markdown-formatted inline code."""
56   return '`%s`' % md_escape(raw_text, characters='`')
57
58
59 def md_italic(raw_text):
60   """Returns markdown-formatted italic text."""
61   return '*%s*' % md_escape(raw_text, characters='*')
62
63
64 def md_link(link_text, link_target):
65   """returns a markdown-formatted link."""
66   return '[%s](%s)' % (md_escape(link_text, characters=']'),
67                        md_escape(link_target, characters=')'))
68
69
70 class MarkdownHelpFormatter(argparse.HelpFormatter):
71   """A really bare-bones argparse help formatter that generates valid markdown.
72
73   This will generate something like:
74
75   usage
76
77   # **section heading**:
78
79   ## **--argument-one**
80
81   ```
82   argument-one help text
83   ```
84
85   """
86
87   #override
88   def _format_usage(self, usage, actions, groups, prefix):
89     usage_text = super(MarkdownHelpFormatter, self)._format_usage(
90         usage, actions, groups, prefix)
91     return md_code(usage_text, language=None)
92
93   #override
94   def format_help(self):
95     self._root_section.heading = md_heading(self._prog, level=1)
96     return super(MarkdownHelpFormatter, self).format_help()
97
98   #override
99   def start_section(self, heading):
100     super(MarkdownHelpFormatter, self).start_section(
101         md_heading(heading, level=2))
102
103   #override
104   def _format_action(self, action):
105     lines = []
106     action_header = self._format_action_invocation(action)
107     lines.append(md_heading(action_header, level=3))
108     if action.help:
109       lines.append(md_code(self._expand_help(action), language=None))
110     lines.extend(['', ''])
111     return '\n'.join(lines)
112
113
114 class MarkdownHelpAction(argparse.Action):
115   def __init__(self,
116                option_strings,
117                dest=argparse.SUPPRESS,
118                default=argparse.SUPPRESS,
119                **kwargs):
120     super(MarkdownHelpAction, self).__init__(
121         option_strings=option_strings,
122         dest=dest,
123         default=default,
124         nargs=0,
125         **kwargs)
126
127   def __call__(self, parser, namespace, values, option_string=None):
128     parser.formatter_class = MarkdownHelpFormatter
129     parser.print_help()
130     parser.exit()
131
132
133 def add_md_help_argument(parser):
134   """Adds --md-help to the given argparse.ArgumentParser.
135
136   Running a script with --md-help will print the help text for that script
137   as valid markdown.
138
139   Args:
140     parser: The ArgumentParser to which --md-help should be added.
141   """
142   parser.add_argument(
143       '--md-help',
144       action=MarkdownHelpAction,
145       help='print Markdown-formatted help text and exit.')
146
147
148 def load_module_from_path(module_path):
149   """Load a module given only the path name.
150
151   Also loads package modules as necessary.
152
153   Args:
154     module_path: An absolute path to a python module.
155   Returns:
156     The module object for the given path.
157   """
158   module_names = [os.path.splitext(os.path.basename(module_path))[0]]
159   d = os.path.dirname(module_path)
160
161   while os.path.exists(os.path.join(d, '__init__.py')):
162     module_names.append(os.path.basename(d))
163     d = os.path.dirname(d)
164
165   d = [d]
166
167   module = None
168   full_module_name = ''
169   for package_name in reversed(module_names):
170     if module:
171       d = module.__path__
172       full_module_name += '.'
173     r = imp.find_module(package_name, d)
174     full_module_name += package_name
175     module = imp.load_module(full_module_name, *r)
176   return module
177
178
179 def md_module(module_obj, module_link=None):
180   """Write markdown documentation for a module.
181
182   Documents public classes and functions.
183
184   Args:
185     module_obj: a module object that should be documented.
186   Returns:
187     A list of markdown-formatted lines.
188   """
189
190   def should_doc(name):
191     return (not isinstance(module_obj.__dict__[name], types.ModuleType)
192             and not name.startswith('_'))
193
194   stuff_to_doc = [
195       obj for name, obj in sorted(module_obj.__dict__.items())
196       if should_doc(name)
197   ]
198
199   classes_to_doc = []
200   functions_to_doc = []
201
202   for s in stuff_to_doc:
203     if isinstance(s, type):
204       classes_to_doc.append(s)
205     elif isinstance(s, types.FunctionType):
206       functions_to_doc.append(s)
207
208   heading_text = module_obj.__name__
209   if module_link:
210     heading_text = md_link(heading_text, module_link)
211
212   content = [
213       md_heading(heading_text, level=1),
214       '',
215       md_italic('This page was autogenerated. '
216                 'Run `devil/bin/generate_md_docs` to update'),
217       '',
218   ]
219
220   for c in classes_to_doc:
221     content += md_class(c)
222   for f in functions_to_doc:
223     content += md_function(f)
224
225   print('\n'.join(content))
226
227   return 0
228
229
230 def md_class(class_obj):
231   """Write markdown documentation for a class.
232
233   Documents public methods. Does not currently document subclasses.
234
235   Args:
236     class_obj: a types.TypeType object for the class that should be
237       documented.
238   Returns:
239     A list of markdown-formatted lines.
240   """
241   content = [md_heading(md_escape(class_obj.__name__), level=2)]
242   content.append('')
243   if class_obj.__doc__:
244     content.extend(md_docstring(class_obj.__doc__))
245
246   def should_doc(name, obj):
247     return (isinstance(obj, types.FunctionType)
248             and (name.startswith('__') or not name.startswith('_')))
249
250   methods_to_doc = [
251       obj for name, obj in sorted(class_obj.__dict__.items())
252       if should_doc(name, obj)
253   ]
254
255   for m in methods_to_doc:
256     content.extend(md_function(m, class_obj=class_obj))
257
258   return content
259
260
261 def md_docstring(docstring):
262   """Write a markdown-formatted docstring.
263
264   Returns:
265     A list of markdown-formatted lines.
266   """
267   content = []
268   lines = textwrap.dedent(docstring).splitlines()
269   content.append(md_escape(lines[0]))
270   lines = lines[1:]
271   while lines and (not lines[0] or lines[0].isspace()):
272     lines = lines[1:]
273
274   if not all(l.isspace() for l in lines):
275     content.append(md_code('\n'.join(lines), language=None))
276     content.append('')
277   return content
278
279
280 def md_function(func_obj, class_obj=None):
281   """Write markdown documentation for a function.
282
283   Args:
284     func_obj: a types.FunctionType object for the function that should be
285       documented.
286   Returns:
287     A list of markdown-formatted lines.
288   """
289   if class_obj:
290     heading_text = '%s.%s' % (class_obj.__name__, func_obj.__name__)
291   else:
292     heading_text = func_obj.__name__
293   content = [md_heading(md_escape(heading_text), level=3)]
294   content.append('')
295
296   if func_obj.__doc__:
297     content.extend(md_docstring(func_obj.__doc__))
298
299   return content
300
301
302 def main(raw_args):
303   """Write markdown documentation for the module at the provided path.
304
305   Args:
306     raw_args: the raw command-line args. Usually sys.argv[1:].
307   Returns:
308     An integer exit code. 0 for success, non-zero for failure.
309   """
310   parser = argparse.ArgumentParser()
311   parser.add_argument('--module-link')
312   parser.add_argument('module_path', type=os.path.realpath)
313   args = parser.parse_args(raw_args)
314
315   return md_module(
316       load_module_from_path(args.module_path), module_link=args.module_link)
317
318
319 if __name__ == '__main__':
320   sys.exit(main(sys.argv[1:]))