-
-
Save Boldewyn/ad1fc1abe234f2e0fbcc to your computer and use it in GitHub Desktop.
Enhanced Markdown viewer
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
#!/usr/bin/env python3 | |
"""Render Markdown and show the result in the browser | |
""" | |
import argparse | |
from http.server import HTTPServer, BaseHTTPRequestHandler | |
from markdown import markdown | |
import os | |
import sys | |
from time import sleep | |
import webbrowser | |
refresh_rate = 1000 # ms | |
tpl = """<!DOCTYPE html> | |
<html> | |
<head> | |
<meta charset=utf-8> | |
<title>%(title)s</title> | |
%(style)s | |
</head> | |
<body class="markdown-body"> | |
<main>%(content)s</main> | |
<footer> | |
<hr> | |
<small>%(filename)s</small> | |
</footer> | |
%(script)s | |
</body> | |
</html>""" | |
tpl_script = """<script> | |
var refresh_rate = %(refresh_rate)s; | |
var canvas = document.getElementsByTagName('main')[0]; | |
var last_fetch_data = canvas.innerHTML; | |
function refresh() { | |
fetch(window.location.href) | |
.then(function(res) { | |
if (! res.ok) { | |
throw res.statusText; | |
} | |
return res.text(); | |
}) | |
.then(function(text) { | |
if (text.indexOf('<main>') === -1) { | |
throw 'no content found'; | |
} | |
var new_fetch_data = text.slice(text.indexOf('<main>') + 6, | |
text.indexOf('</main>')); | |
if (new_fetch_data != last_fetch_data) { | |
last_fetch_data = new_fetch_data; | |
canvas.innerHTML = new_fetch_data; | |
} | |
window.setTimeout(refresh, refresh_rate); | |
}) | |
.catch(function(err) { | |
var txt = 'ERROR'; | |
if (err) { | |
txt += ': ' + err; | |
} | |
txt += ' Live reloading was stopped.'; | |
canvas.innerHTML = '<p style="color:red">'+txt+'</p>' + | |
canvas.innerHTML; | |
}) | |
; | |
} | |
window.setTimeout(refresh, refresh_rate); | |
</script>""" | |
class ReqHandler(BaseHTTPRequestHandler): | |
def log_request(self, code='-', size='-'): | |
if self.server.settings.verbose: | |
super(BaseHTTPRequestHandler, self).log_request(self, code, size) | |
def do_GET(self): | |
self.send_response(200) | |
self.send_header("Content-type", "text/html") | |
self.end_headers() | |
self.wfile.write(render(self.server.settings).encode('utf-8')) | |
class MDStop(Exception): | |
pass | |
class MDSettings(): | |
filename = None | |
port = 15432 | |
timeout = None | |
refresh = False | |
verbose = False | |
style = ( | |
'<link rel="stylesheet"' | |
' href="https://rawgit.com/sindresorhus/github-markdown-css/' | |
'gh-pages/github-markdown.css">' | |
'<style>main{max-width:960px;margin:0 auto;}</style>' | |
) | |
mdextensions = ['codehilite'] | |
mdextension_configs = { | |
'codehilite': { | |
'noclasses': True, | |
#'pygments_style': 'monokai', | |
} | |
} | |
class MDServer(HTTPServer): | |
settings = None | |
def handle_timeout(self): | |
if self.settings.verbose: | |
print("Connection ended. Shutting down...") | |
raise MDStop() | |
def render(settings): | |
"""render markdown""" | |
global tpl | |
global tpl_script | |
global refresh_rate | |
filename = settings.filename | |
if not os.path.isfile(filename): | |
# race condition: Is file saved right now? Wait 100ms and check | |
# again. | |
sleep(.1) | |
if not os.path.isfile(filename): | |
raise FileExistsError() | |
with open(filename) as f: | |
script = '' | |
if settings.refresh: | |
script = tpl_script % { | |
'refresh_rate': refresh_rate, | |
} | |
mddata = f.read() | |
title = os.path.abspath(filename) | |
# TODO: extract title from mddata | |
buffer = tpl % { | |
'filename': os.path.abspath(filename), | |
'title': title, | |
'content': markdown(mddata, | |
extensions=settings.mdextensions, | |
extension_configs=settings.mdextension_configs, | |
output_format='html5'), | |
'style': settings.style, | |
'script': script, | |
} | |
return buffer | |
def serve(settings): | |
"""serve the rendered HTML via simple webserver""" | |
port = settings.port | |
httpd = None | |
while not httpd: | |
try: | |
httpd = MDServer(('', port), ReqHandler) | |
except OSError: | |
httpd = None | |
port += 1 | |
if port > 50000: | |
print('Giving up finding an open port...') | |
sys.exit(1) | |
httpd.settings = settings | |
if settings.timeout: | |
httpd.timeout = settings.timeout | |
webbrowser.open("http://localhost:%(port)s" % { 'port': port }, 2, True) | |
try: | |
while True: | |
httpd.handle_request() | |
except (KeyboardInterrupt, FileExistsError, MDStop): | |
httpd.socket.close() | |
def main(args): | |
global refresh_rate | |
settings = MDSettings() | |
parser = argparse.ArgumentParser(description=__doc__) | |
parser.add_argument('filename', metavar='FILE', | |
help='Markdown file to render') | |
parser.add_argument('--dump', '-d', action="store_true", default=False, | |
help='dump result to stdout instead of serving') | |
parser.add_argument('--no-refresh', dest='refresh', action="store_false", | |
default=True, | |
help='do not auto-refresh the content when edited') | |
parser.add_argument('--port', '-p', type=int, default=settings.port, | |
help='the port to bind to. If it is busy, increment in steps of ' | |
'1. Default port is %(default)s.') | |
parser.add_argument('--style', '-s', metavar='URL', default=settings.style, | |
help='stylesheet URL (or markup) to embed. Default is a ' | |
'stylesheet resembling Github’s Markdown style') | |
parser.add_argument('--verbose', '-v', action="store_true", default=False, | |
help='be verbose on the command line') | |
args = parser.parse_args(args) | |
if args.dump: | |
args.refresh = False | |
settings.filename = args.filename | |
settings.port = args.port | |
settings.refresh = args.refresh | |
if args.refresh: | |
# allow one skip and a bit of latency, then time out | |
settings.timeout = refresh_rate * 2.1 / 1000 | |
# set no timeout, when autorefresh is disabled, so that reloading | |
# in the browser works. | |
settings.verbose = args.verbose | |
if args.style: | |
if args.style.startswith('http:') or args.style.startswith('https:'): | |
settings.style = '<link rel="stylesheet" href="%s">' % args.style | |
else: | |
# assume it's full-fledged HTML | |
settings.style = args.style | |
if args.dump: | |
print(render(settings)) | |
sys.exit(0) | |
else: | |
serve(settings) | |
if __name__ == "__main__": | |
main(sys.argv[1:]) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment