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