Skip to content

Instantly share code, notes, and snippets.

@Boldewyn
Last active January 13, 2016 10:53
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save Boldewyn/ad1fc1abe234f2e0fbcc to your computer and use it in GitHub Desktop.
Save Boldewyn/ad1fc1abe234f2e0fbcc to your computer and use it in GitHub Desktop.
Enhanced Markdown viewer
#!/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