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