| #!/usr/bin/env python |
| # Copyright 2015 The Chromium Authors. All rights reserved. |
| # Use of this source code is governed by a BSD-style license that can be |
| # found in the LICENSE file. |
| |
| """Simple Markdown browser for a Git checkout.""" |
| from __future__ import print_function |
| |
| import SimpleHTTPServer |
| import SocketServer |
| import argparse |
| import codecs |
| import os |
| import re |
| import socket |
| import sys |
| import threading |
| import time |
| import webbrowser |
| from xml.etree import ElementTree |
| |
| |
| THIS_DIR = os.path.realpath(os.path.dirname(__file__)) |
| SRC_DIR = os.path.dirname(os.path.dirname(THIS_DIR)) |
| sys.path.insert(0, os.path.join(SRC_DIR, 'third_party', 'Python-Markdown')) |
| import markdown |
| |
| |
| def main(argv): |
| parser = argparse.ArgumentParser(prog='md_browser') |
| parser.add_argument('-p', '--port', type=int, default=8080, |
| help='port to run on (default = %(default)s)') |
| parser.add_argument('-d', '--directory', type=str, default=SRC_DIR) |
| parser.add_argument('file', nargs='?', |
| help='open file in browser') |
| args = parser.parse_args(argv) |
| |
| top_level = os.path.realpath(args.directory) |
| |
| s = Server(args.port, top_level) |
| |
| print('Listening on http://localhost:%s/' % args.port) |
| thread = None |
| if args.file: |
| path = os.path.realpath(args.file) |
| if not path.startswith(top_level): |
| print('%s is not under %s' % (args.file, args.directory)) |
| return 1 |
| rpath = os.path.relpath(path, top_level) |
| url = 'http://localhost:%d/%s' % (args.port, rpath) |
| print('Opening %s' % url) |
| thread = threading.Thread(target=_open_url, args=(url,)) |
| thread.start() |
| |
| elif os.path.isfile(os.path.join(top_level, 'docs', 'README.md')): |
| print(' Try loading http://localhost:%d/docs/README.md' % args.port) |
| elif os.path.isfile(os.path.join(args.directory, 'README.md')): |
| print(' Try loading http://localhost:%d/README.md' % args.port) |
| |
| retcode = 1 |
| try: |
| s.serve_forever() |
| except KeyboardInterrupt: |
| retcode = 130 |
| except Exception as e: |
| print('Exception raised: %s' % str(e)) |
| |
| s.shutdown() |
| if thread: |
| thread.join() |
| return retcode |
| |
| |
| def _open_url(url): |
| time.sleep(1) |
| webbrowser.open(url) |
| |
| |
| def _gitiles_slugify(value, _separator): |
| """Convert a string (representing a section title) to URL anchor name. |
| |
| This function is passed to "toc" extension as an extension option, so we |
| can emulate the way how Gitiles converts header titles to URL anchors. |
| |
| Gitiles' official documentation about the conversion is at: |
| |
| https://gerrit.googlesource.com/gitiles/+/master/Documentation/markdown.md#Named-anchors |
| |
| Args: |
| value: The name of a section that is to be converted. |
| _separator: Unused. This is actually a configurable string that is used |
| as a replacement character for spaces in the title, typically set to |
| '-'. Since we emulate Gitiles' way of slugification here, it makes |
| little sense to have the separator charactor configurable. |
| """ |
| |
| # TODO(yutak): Implement accent removal. This does not seem easy without |
| # some library. For now we just make accented characters turn into |
| # underscores, just like other non-ASCII characters. |
| |
| value = value.encode('ascii', 'replace') # Non-ASCII turns into '?'. |
| value = re.sub(r'[^- a-zA-Z0-9]', '_', value) # Non-alphanumerics to '_'. |
| value = value.replace(u' ', u'-') |
| value = re.sub(r'([-_])[-_]+', r'\1', value) # Fold hyphens and underscores. |
| return value |
| |
| |
| class Server(SocketServer.TCPServer): |
| def __init__(self, port, top_level): |
| SocketServer.TCPServer.__init__(self, ('0.0.0.0', port), Handler) |
| self.port = port |
| self.top_level = top_level |
| self.retcode = None |
| |
| def server_bind(self): |
| self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) |
| self.socket.bind(self.server_address) |
| |
| |
| class Handler(SimpleHTTPServer.SimpleHTTPRequestHandler): |
| def do_GET(self): |
| path = self.path |
| |
| # strip off the repo and branch info, if present, for compatibility |
| # with gitiles. |
| if path.startswith('/chromium/src/+/master'): |
| path = path[len('/chromium/src/+/master'):] |
| |
| full_path = os.path.realpath(os.path.join(self.server.top_level, path[1:])) |
| |
| if not full_path.startswith(self.server.top_level): |
| self._DoUnknown() |
| elif path == '/doc.css': |
| self._DoCSS('doc.css') |
| elif not os.path.exists(full_path): |
| self._DoNotFound() |
| elif path.lower().endswith('.md'): |
| self._DoMD(path) |
| elif os.path.exists(full_path + '/README.md'): |
| self._DoMD(path + '/README.md') |
| else: |
| self._DoUnknown() |
| |
| def _DoMD(self, path): |
| extensions = [ |
| 'markdown.extensions.def_list', |
| 'markdown.extensions.fenced_code', |
| 'markdown.extensions.tables', |
| 'markdown.extensions.toc', |
| 'gitiles_ext_blocks', |
| ] |
| extension_configs = { |
| 'markdown.extensions.toc': { |
| 'slugify': _gitiles_slugify |
| }, |
| } |
| |
| contents = self._Read(path[1:]) |
| |
| md = markdown.Markdown(extensions=extensions, |
| extension_configs=extension_configs, |
| output_format='html4') |
| |
| has_a_single_h1 = (len([line for line in contents.splitlines() |
| if (line.startswith('#') and |
| not line.startswith('##'))]) == 1) |
| |
| md.treeprocessors['adjust_toc'] = _AdjustTOC(has_a_single_h1) |
| |
| md_fragment = md.convert(contents).encode('utf-8') |
| |
| try: |
| self._WriteHeader('text/html') |
| self._WriteTemplate('header.html') |
| self.wfile.write(md_fragment) |
| self._WriteTemplate('footer.html') |
| except: |
| raise |
| |
| def _DoCSS(self, template): |
| self._WriteHeader('text/css') |
| self._WriteTemplate(template) |
| |
| def _DoNotFound(self): |
| self._WriteHeader('text/html', status_code=404) |
| self.wfile.write('<html><body>%s not found</body></html>' % self.path) |
| |
| def _DoUnknown(self): |
| self._WriteHeader('text/html') |
| self.wfile.write('<html><body>I do not know how to serve %s.</body>' |
| '</html>' % self.path) |
| |
| def _Read(self, relpath, relative_to=None): |
| if relative_to is None: |
| relative_to = self.server.top_level |
| assert not relpath.startswith(os.sep) |
| path = os.path.join(relative_to, relpath) |
| with codecs.open(path, encoding='utf-8') as fp: |
| return fp.read() |
| |
| def _WriteHeader(self, content_type='text/plain', status_code=200): |
| self.send_response(status_code) |
| self.send_header('Content-Type', content_type) |
| self.end_headers() |
| |
| def _WriteTemplate(self, template): |
| contents = self._Read(os.path.join('tools', 'md_browser', template), |
| relative_to=SRC_DIR) |
| self.wfile.write(contents.encode('utf-8')) |
| |
| |
| class _AdjustTOC(markdown.treeprocessors.Treeprocessor): |
| def __init__(self, has_a_single_h1): |
| super(_AdjustTOC, self).__init__() |
| self.has_a_single_h1 = has_a_single_h1 |
| |
| def run(self, tree): |
| # Given |
| # |
| # # H1 |
| # |
| # [TOC] |
| # |
| # ## first H2 |
| # |
| # ## second H2 |
| # |
| # the markdown.extensions.toc extension generates: |
| # |
| # <div class='toc'> |
| # <ul><li><a>H1</a> |
| # <ul><li>first H2 |
| # <li>second H2</li></ul></li><ul></div> |
| # |
| # for [TOC]. But, we want the TOC to have its own subheading, so |
| # we rewrite <div class='toc'><ul>...</ul></div> to: |
| # |
| # <div class='toc'> |
| # <h2>Contents</h2> |
| # <div class='toc-aux'> |
| # <ul>...</ul></div></div> |
| # |
| # In addition, if the document only has a single H1, it is usually the |
| # title, and we don't want the title to be in the TOC. So, we remove it |
| # and shift all of the title's children up a level, leaving: |
| # |
| # <div class='toc'> |
| # <h2>Contents</h2> |
| # <div class='toc-aux'> |
| # <ul><li>first H2 |
| # <li>second H2</li></ul></div></div> |
| |
| for toc_node in tree.findall(".//*[@class='toc']"): |
| toc_ul = toc_node[0] |
| if self.has_a_single_h1: |
| toc_ul_li = toc_ul[0] |
| ul_with_the_desired_toc_entries = toc_ul_li[1] |
| else: |
| ul_with_the_desired_toc_entries = toc_ul |
| |
| toc_node.remove(toc_ul) |
| contents = ElementTree.SubElement(toc_node, 'h2') |
| contents.text = 'Contents' |
| contents.tail = '\n' |
| toc_aux = ElementTree.SubElement(toc_node, 'div', {'class': 'toc-aux'}) |
| toc_aux.text = '\n' |
| toc_aux.append(ul_with_the_desired_toc_entries) |
| toc_aux.tail = '\n' |
| |
| |
| if __name__ == '__main__': |
| sys.exit(main(sys.argv[1:])) |