• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
6from __future__ import print_function
7
8import argparse
9import imp
10import os
11import re
12import sys
13import textwrap
14import 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(os.path.join(
23    os.path.dirname(__file__), '..', '..'))
24
25
26def md_bold(raw_text):
27  """Returns markdown-formatted bold text."""
28  return '**%s**' % md_escape(raw_text, characters='*')
29
30
31def 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 '',
35      code=md_escape(raw_text, characters='`'))
36
37
38def md_escape(raw_text, characters='*_'):
39  """Escapes * and _."""
40  def escape_char(m):
41    return '\\%s' % m.group(0)
42  pattern = '[%s]' % re.escape(characters)
43  return re.sub(pattern, escape_char, raw_text)
44
45
46def md_heading(raw_text, level):
47  """Returns markdown-formatted heading."""
48  adjusted_level = min(max(level, 0), 6)
49  return '%s%s%s' % (
50      '#' * adjusted_level, ' ' if adjusted_level > 0 else '', raw_text)
51
52
53def md_inline_code(raw_text):
54  """Returns markdown-formatted inline code."""
55  return '`%s`' % md_escape(raw_text, characters='`')
56
57
58def md_italic(raw_text):
59  """Returns markdown-formatted italic text."""
60  return '*%s*' % md_escape(raw_text, characters='*')
61
62
63def md_link(link_text, link_target):
64  """returns a markdown-formatted link."""
65  return '[%s](%s)' % (
66      md_escape(link_text, characters=']'),
67      md_escape(link_target, characters=')'))
68
69
70class 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
114class MarkdownHelpAction(argparse.Action):
115  def __init__(self, option_strings,
116               dest=argparse.SUPPRESS, default=argparse.SUPPRESS,
117               **kwargs):
118    super(MarkdownHelpAction, self).__init__(
119        option_strings=option_strings,
120        dest=dest,
121        default=default,
122        nargs=0,
123        **kwargs)
124
125  def __call__(self, parser, namespace, values, option_string=None):
126    parser.formatter_class = MarkdownHelpFormatter
127    parser.print_help()
128    parser.exit()
129
130
131def add_md_help_argument(parser):
132  """Adds --md-help to the given argparse.ArgumentParser.
133
134  Running a script with --md-help will print the help text for that script
135  as valid markdown.
136
137  Args:
138    parser: The ArgumentParser to which --md-help should be added.
139  """
140  parser.add_argument('--md-help', action=MarkdownHelpAction,
141                      help='print Markdown-formatted help text and exit.')
142
143
144def load_module_from_path(module_path):
145  """Load a module given only the path name.
146
147  Also loads package modules as necessary.
148
149  Args:
150    module_path: An absolute path to a python module.
151  Returns:
152    The module object for the given path.
153  """
154  module_names = [os.path.splitext(os.path.basename(module_path))[0]]
155  d = os.path.dirname(module_path)
156
157  while os.path.exists(os.path.join(d, '__init__.py')):
158    module_names.append(os.path.basename(d))
159    d = os.path.dirname(d)
160
161  d = [d]
162
163  module = None
164  full_module_name = ''
165  for package_name in reversed(module_names):
166    if module:
167      d = module.__path__
168      full_module_name += '.'
169    r = imp.find_module(package_name, d)
170    full_module_name += package_name
171    module = imp.load_module(full_module_name, *r)
172  return module
173
174
175def md_module(module_obj, module_path=None, module_link=None):
176  """Write markdown documentation for a class.
177
178  Documents public classes and functions.
179
180  Args:
181    class_obj: a types.TypeType object for the class that should be
182      documented.
183  Returns:
184    A list of markdown-formatted lines.
185  """
186  def should_doc(name):
187    return (not isinstance(module_obj.__dict__[name], types.ModuleType)
188            and not name.startswith('_'))
189
190  stuff_to_doc = sorted(
191    obj for name, obj in module_obj.__dict__.iteritems()
192    if should_doc(name))
193
194  classes_to_doc = []
195  functions_to_doc = []
196
197  for s in stuff_to_doc:
198    if isinstance(s, types.TypeType):
199      classes_to_doc.append(s)
200    elif isinstance(s, types.FunctionType):
201      functions_to_doc.append(s)
202
203  command = ['devil/utils/markdown.py']
204  if module_link:
205    command.extend(['--module-link', module_link])
206  if module_path:
207    command.append(os.path.relpath(module_path, _DEVIL_ROOT))
208
209  heading_text = module_obj.__name__
210  if module_link:
211    heading_text = md_link(heading_text, module_link)
212
213  content = [
214      md_heading(heading_text, level=1),
215      '',
216      md_italic('This page was autogenerated by %s'
217          % md_inline_code(' '.join(command))),
218      '',
219  ]
220
221  for c in classes_to_doc:
222    content += md_class(c)
223  for f in functions_to_doc:
224    content += md_function(f)
225
226  print('\n'.join(content))
227
228  return 0
229
230
231def md_class(class_obj):
232  """Write markdown documentation for a class.
233
234  Documents public methods. Does not currently document subclasses.
235
236  Args:
237    class_obj: a types.TypeType object for the class that should be
238      documented.
239  Returns:
240    A list of markdown-formatted lines.
241  """
242  content = [md_heading(md_escape(class_obj.__name__), level=2)]
243  content.append('')
244  if class_obj.__doc__:
245    content.extend(md_docstring(class_obj.__doc__))
246
247  def should_doc(name, obj):
248    return (isinstance(obj, types.FunctionType)
249            and (name.startswith('__') or not name.startswith('_')))
250
251  methods_to_doc = sorted(
252      obj for name, obj in class_obj.__dict__.iteritems()
253      if should_doc(name, obj))
254
255  for m in methods_to_doc:
256    content.extend(md_function(m, class_obj=class_obj))
257
258  return content
259
260
261def 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
280def 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
302def 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),
317      module_link=args.module_link)
318
319
320if __name__ == '__main__':
321  sys.exit(main(sys.argv[1:]))
322
323