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.
6 from __future__ import print_function
16 # A markdown code block template: https://goo.gl/9EsyRi
17 _CODE_BLOCK_FORMAT = '''```{language}
22 _DEVIL_ROOT = os.path.abspath(
23 os.path.join(os.path.dirname(__file__), '..', '..'))
26 def md_bold(raw_text):
27 """Returns markdown-formatted bold text."""
28 return '**%s**' % md_escape(raw_text, characters='*')
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='`'))
37 def md_escape(raw_text, characters='*_'):
38 """Escapes * and _."""
41 return '\\%s' % m.group(0)
43 pattern = '[%s]' % re.escape(characters)
44 return re.sub(pattern, escape_char, raw_text)
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 '',
54 def md_inline_code(raw_text):
55 """Returns markdown-formatted inline code."""
56 return '`%s`' % md_escape(raw_text, characters='`')
59 def md_italic(raw_text):
60 """Returns markdown-formatted italic text."""
61 return '*%s*' % md_escape(raw_text, characters='*')
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=')'))
70 class MarkdownHelpFormatter(argparse.HelpFormatter):
71 """A really bare-bones argparse help formatter that generates valid markdown.
73 This will generate something like:
77 # **section heading**:
82 argument-one help text
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)
94 def format_help(self):
95 self._root_section.heading = md_heading(self._prog, level=1)
96 return super(MarkdownHelpFormatter, self).format_help()
99 def start_section(self, heading):
100 super(MarkdownHelpFormatter, self).start_section(
101 md_heading(heading, level=2))
104 def _format_action(self, action):
106 action_header = self._format_action_invocation(action)
107 lines.append(md_heading(action_header, level=3))
109 lines.append(md_code(self._expand_help(action), language=None))
110 lines.extend(['', ''])
111 return '\n'.join(lines)
114 class MarkdownHelpAction(argparse.Action):
117 dest=argparse.SUPPRESS,
118 default=argparse.SUPPRESS,
120 super(MarkdownHelpAction, self).__init__(
121 option_strings=option_strings,
127 def __call__(self, parser, namespace, values, option_string=None):
128 parser.formatter_class = MarkdownHelpFormatter
133 def add_md_help_argument(parser):
134 """Adds --md-help to the given argparse.ArgumentParser.
136 Running a script with --md-help will print the help text for that script
140 parser: The ArgumentParser to which --md-help should be added.
144 action=MarkdownHelpAction,
145 help='print Markdown-formatted help text and exit.')
148 def load_module_from_path(module_path):
149 """Load a module given only the path name.
151 Also loads package modules as necessary.
154 module_path: An absolute path to a python module.
156 The module object for the given path.
158 module_names = [os.path.splitext(os.path.basename(module_path))[0]]
159 d = os.path.dirname(module_path)
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)
168 full_module_name = ''
169 for package_name in reversed(module_names):
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)
179 def md_module(module_obj, module_link=None):
180 """Write markdown documentation for a module.
182 Documents public classes and functions.
185 module_obj: a module object that should be documented.
187 A list of markdown-formatted lines.
190 def should_doc(name):
191 return (not isinstance(module_obj.__dict__[name], types.ModuleType)
192 and not name.startswith('_'))
195 obj for name, obj in sorted(module_obj.__dict__.items())
200 functions_to_doc = []
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)
208 heading_text = module_obj.__name__
210 heading_text = md_link(heading_text, module_link)
213 md_heading(heading_text, level=1),
215 md_italic('This page was autogenerated. '
216 'Run `devil/bin/generate_md_docs` to update'),
220 for c in classes_to_doc:
221 content += md_class(c)
222 for f in functions_to_doc:
223 content += md_function(f)
225 print('\n'.join(content))
230 def md_class(class_obj):
231 """Write markdown documentation for a class.
233 Documents public methods. Does not currently document subclasses.
236 class_obj: a types.TypeType object for the class that should be
239 A list of markdown-formatted lines.
241 content = [md_heading(md_escape(class_obj.__name__), level=2)]
243 if class_obj.__doc__:
244 content.extend(md_docstring(class_obj.__doc__))
246 def should_doc(name, obj):
247 return (isinstance(obj, types.FunctionType)
248 and (name.startswith('__') or not name.startswith('_')))
251 obj for name, obj in sorted(class_obj.__dict__.items())
252 if should_doc(name, obj)
255 for m in methods_to_doc:
256 content.extend(md_function(m, class_obj=class_obj))
261 def md_docstring(docstring):
262 """Write a markdown-formatted docstring.
265 A list of markdown-formatted lines.
268 lines = textwrap.dedent(docstring).splitlines()
269 content.append(md_escape(lines[0]))
271 while lines and (not lines[0] or lines[0].isspace()):
274 if not all(l.isspace() for l in lines):
275 content.append(md_code('\n'.join(lines), language=None))
280 def md_function(func_obj, class_obj=None):
281 """Write markdown documentation for a function.
284 func_obj: a types.FunctionType object for the function that should be
287 A list of markdown-formatted lines.
290 heading_text = '%s.%s' % (class_obj.__name__, func_obj.__name__)
292 heading_text = func_obj.__name__
293 content = [md_heading(md_escape(heading_text), level=3)]
297 content.extend(md_docstring(func_obj.__doc__))
303 """Write markdown documentation for the module at the provided path.
306 raw_args: the raw command-line args. Usually sys.argv[1:].
308 An integer exit code. 0 for success, non-zero for failure.
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)
316 load_module_from_path(args.module_path), module_link=args.module_link)
319 if __name__ == '__main__':
320 sys.exit(main(sys.argv[1:]))