Skip to content

Instantly share code, notes, and snippets.

@tkalus
Created March 13, 2022 22:15
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save tkalus/67edfa0487cd808499305d681aa32c0e to your computer and use it in GitHub Desktop.
Save tkalus/67edfa0487cd808499305d681aa32c0e to your computer and use it in GitHub Desktop.
Flask render markdown with live-reload
#!/usr/bin/env python3
"""
Render a markdown file into HTML via Flask app on localhost.
Includes live-reload using HTTP long polling and a bit of vanilla JS.
Made pretty with https://picocss.com
[1]$ pip install Flask Flask-Caching Flask-Markdown requests
[1]$ flask run
[2]$ open http://localhost:5000/README.md
"""
import base64
import json
import os
import sys
import threading
import time
from http import HTTPStatus
import flask
import flask_caching
import flaskext.markdown
import requests
import werkzeug.datastructures
app = flask.Flask(__name__)
app.config.from_mapping(
{
"CACHE_DEFAULT_TIMEOUT": 300,
"CACHE_TYPE": "SimpleCache",
}
)
cache = flask_caching.Cache(app)
flaskext.markdown.Markdown(
app,
extensions=["attr_list", "md_in_html"],
)
HTML_TEMPLATE = """
<!doctype html>
<html data-theme="light">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=0.95" />
<link rel="icon" href="data:;base64,iVBORw0KGgo=" />
<link rel="stylesheet" href="/css/pico.fluid.classless.min.css" />
<style>
main {
box-sizing: border-box;
min-width: 300px;
max-width: 980px;
margin: 0 auto;
padding: 45px;
}
@media (max-width: 767px) {
main {padding: 15px;}
}
</style>
</head>
<body>
<main>
{{ content | markdown }}
</main>
<script>
let timestamp_url = "{{ timestamp_url }}"
async function subscribe() {
let response = await fetch(timestamp_url).catch(function (err) {});
if (!response || ![200, 204].includes(response.status)) {
// some flavor of failure of some sort -- probably Flask reload.
await new Promise(resolve => setTimeout(resolve, 1000));
await subscribe();
} else if (response.status === 204) {
// long poll completed with no updates; re-poll.
await subscribe();
} else {
// new content!; let's reload.
location.reload()
}
}
subscribe();
</script>
</body>
</html>
"""
@app.route("/css/pico.<ext>")
@cache.memoize(1750)
def css(ext) -> flask.typing.ResponseReturnValue:
url = f"https://unpkg.com/@picocss/pico@latest/css/pico.{ext}"
req_resp = requests.get(url=url)
resp = flask.Response(
response=req_resp.text,
status=req_resp.status_code,
mimetype=req_resp.headers["content-type"],
)
resp.cache_control.max_age = 1800
return resp
@app.route("/<pathname>")
def index(pathname) -> flask.typing.ResponseReturnValue:
if not os.path.exists(pathname):
return flask.Response(status=HTTPStatus.NOT_FOUND)
# Convenience to get latest mtime of target file at pathname
mtime = lambda: int(os.path.getmtime(pathname))
if ts := int(flask.request.args.get("ts", "0")): # if not present, 0 evals to False
# If we have a ts= query param, use tha as the timestamp to check against.
block_secs = 20 # Long poll for 20 seconds
check_per_sec = 5 # check 5x per second
for _ in range(block_secs * check_per_sec):
time.sleep(1 / check_per_sec)
if ts < mtime():
# File changed! JS will get the 200 and call `location.reload();`
return flask.Response(
response="CHANGED",
status=HTTPStatus.OK,
mimetype="text/plain",
)
# File timestamp didn't change before we ended the loop.
# JS will loop-back and long poll again.
return flask.Response(status=HTTPStatus.NO_CONTENT)
content: str = ""
with open(pathname) as f:
content = f.read()
return flask.Response(
response=flask.render_template_string(
source=HTML_TEMPLATE,
content=content,
timestamp_url=f"/{pathname}?ts={mtime()}",
),
status=HTTPStatus.OK,
mimetype="text/html",
)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment