added hot reload and Gevent to uwsgi server

This commit is contained in:
2023-10-18 21:08:23 +08:00
parent 67948f81c4
commit 3626cd7980
19 changed files with 551 additions and 406 deletions

0
src/md2html/__init__.py Normal file
View File

92
src/md2html/file_watch.py Normal file
View File

@@ -0,0 +1,92 @@
from threading import Lock
from typing import Optional, Callable
from os import getcwd
from watchdog.events import PatternMatchingEventHandler, FileSystemEvent, \
FileCreatedEvent, FileModifiedEvent, FileClosedEvent, FileMovedEvent
from watchdog.observers import Observer
import logging
from gevent.event import Event
class Subscription:
def __init__(self, unsubscribe: Callable[['Subscription'], None]):
self._unsubscribe_callback = unsubscribe
self._event: Event = Event()
def unsubscribe(self) -> None:
self._unsubscribe_callback(self)
def wait(self, tout: float) -> bool:
return self._event.wait(tout)
def notify(self) -> None:
self._event.set()
def reset(self) -> None:
self._event.clear()
class FileWatcher(PatternMatchingEventHandler):
def __init__(self, path):
super().__init__(patterns=['*.md'],
ignore_patterns=None,
ignore_directories=False,
case_sensitive=True)
self.subscriptions: dict[str, set[Subscription]] = dict()
self.observer: Observer = Observer()
self.observer.schedule(self, path=path, recursive=True)
self.observer.start()
self.logger = logging.getLogger(FileWatcher.__name__)
self._lock = Lock()
def subscribe(self, path: str) -> Subscription:
subscriptions = self.subscriptions
subscriptions_per_path = subscriptions.setdefault(path, set())
def unsubscribe_callback(subscription):
with self._lock:
subscriptions_per_path.remove(subscription)
result = Subscription(unsubscribe_callback)
subscriptions_per_path.add(result)
return result
def stop(self) -> None:
self.observer.stop()
self.observer.join()
def on_any_event(self, event: FileSystemEvent) -> None:
what = "directory" if event.is_directory else "file"
def notify_subscriptions(path):
with self._lock:
subscriptions = self.subscriptions
subscriptions_per_path = subscriptions.get(path, None)
if subscriptions_per_path:
for s in subscriptions_per_path:
s.notify()
if isinstance(event, FileClosedEvent):
self.logger.debug("Closed %s: %s", what, event.src_path)
# update_subscriptions()
elif isinstance(event, FileMovedEvent):
self.logger.debug("Moved %s: %s to %s", what, event.src_path, event.dest_path)
notify_subscriptions(event.dest_path)
elif isinstance(event, FileCreatedEvent):
self.logger.debug("Created %s: %s", what, event.src_path)
notify_subscriptions(event.src_path)
elif isinstance(event, FileModifiedEvent):
self.logger.debug("Modified %s: %s", what, event.src_path)
notify_subscriptions(event.src_path)
if __name__ == '__main__':
logging.basicConfig(
level=logging.DEBUG,
format='%(asctime)s [%(threadName)s] (%(name)s) %(levelname)s %(message)s'
)
watcher = FileWatcher(getcwd())
watcher.observer.join()

41
src/md2html/md2html.py Normal file
View File

@@ -0,0 +1,41 @@
import sys
from os.path import dirname, join
from time import time
from typing import Optional
import markdown
STATIC_RESOURCES: set[str] = {
'/github-markdown.css',
'/custom.css',
'/hot-reload.js',
'/pygment.css',
}
STATIC_CACHE: dict[str, tuple[str, float]] = {}
MARDOWN_EXTENSIONS = ['extra', 'smarty', 'tables', 'codehilite']
def load_from_cache(path) -> tuple[str, float]:
global STATIC_CACHE
if path not in STATIC_CACHE:
with open(join(dirname(__file__), 'static') + path, 'r') as static_file:
STATIC_CACHE[path] = (static_file.read(), time())
return STATIC_CACHE[path]
def compile_html(mdfile=None,
extensions: Optional[list[str]] = None,
raw: bool = False) -> str:
with mdfile and open(mdfile, 'r') or sys.stdin as instream:
html = markdown.markdown(instream.read(), extensions=extensions, output_format='html')
if raw:
doc = html
else:
css = ' <style>% s\n%s\n%s\n </style>' % (
load_from_cache('/github-markdown.css')[0],
load_from_cache('/pygment.css')[0],
load_from_cache('/custom.css')[0],
)
script = '<script src="/hot-reload.js", type="text/javascript" defer="true"></script>'
doc = load_from_cache('/template.html')[0].format(content=html, script=script, css=css)
return doc

225
src/md2html/server.py Normal file
View File

@@ -0,0 +1,225 @@
import logging
from os import getcwd, listdir
from os.path import exists, splitext, isfile, join, relpath, isdir, basename, getmtime, dirname
from mimetypes import init as mimeinit, guess_type
import hashlib
from .md2html import compile_html, load_from_cache, STATIC_RESOURCES, MARDOWN_EXTENSIONS
from shutil import which
from subprocess import check_output
from io import BytesIO
from typing import Callable, TYPE_CHECKING, BinaryIO, Optional
from .file_watch import FileWatcher
if TYPE_CHECKING:
from _typeshed import StrOrBytesPath
mimeinit()
cwd: 'StrOrBytesPath' = getcwd()
def has_extension(filepath, extension):
_, ext = splitext(filepath)
return ext == extension
def is_markdown(filepath):
return has_extension(filepath, ".md")
def is_dotfile(filepath):
return has_extension(filepath, ".dot")
class Server:
def __init__(self, root_dir: 'StrOrBytesPath' = getcwd()):
self.root_dir = root_dir
self.cache = dict['StrOrBytesPath', tuple[str, float]]()
self.file_watcher = FileWatcher(cwd)
self.logger = logging.getLogger(Server.__name__)
def handle_request(self, method: str, url_path: str, etag: Optional[str], query_string: Optional[str], start_response):
if method != 'GET':
start_response('405', [])
return []
path: 'StrOrBytesPath' = join(self.root_dir, relpath(url_path, '/'))
if url_path in STATIC_RESOURCES:
content, mtime = load_from_cache(url_path)
content = content.encode()
etag, digest = self.compute_etag_and_digest(
etag,
url_path,
lambda: BytesIO(content),
lambda: mtime
)
if etag and etag == digest:
return self.not_modified(start_response, digest, ('Cache-Control', 'must-revalidate, max-age=86400'))
elif content:
mime_type = guess_type(basename(url_path))[0] or 'application/octet-stream'
start_response('200 OK', [
('Content-Type', f'{mime_type}; charset=UTF-8'),
('Etag', 'W/"%s"' % digest),
('Cache-Control', 'must-revalidate, max-age=86400'),
])
return content
elif exists(path):
if isfile(path):
etag, digest = self.compute_etag_and_digest(
etag,
path,
lambda: open(path, 'rb'),
lambda: getmtime(path)
)
if etag and etag == digest:
if is_markdown(path) and query_string == 'reload':
subscription = self.file_watcher.subscribe(path)
try:
has_changed = subscription.wait(30)
if has_changed:
_, digest = self.compute_etag_and_digest(
etag,
path,
lambda: open(path, 'rb'),
lambda: getmtime(path)
)
if etag != digest:
body = compile_html(path,
MARDOWN_EXTENSIONS,
raw=True).encode()
start_response('200 OK', [('Content-Type', 'text/html; charset=UTF-8'),
('Etag', 'W/"%s"' % digest),
('Cache-Control', 'no-cache'),
])
return [body]
finally:
subscription.unsubscribe()
return self.not_modified(start_response, digest)
elif is_markdown(path):
raw = query_string == 'reload'
body = compile_html(path, MARDOWN_EXTENSIONS, raw=raw).encode()
start_response('200 OK', [('Content-Type', 'text/html; charset=UTF-8'),
('Etag', 'W/"%s"' % digest),
('Cache-Control', 'no-cache'),
])
return [body]
elif is_dotfile(path) and which("dot"):
body = check_output(['dot', '-Tsvg', basename(path)], cwd=dirname(path))
start_response('200 OK', [('Content-Type', 'image/svg+xml; charset=UTF-8'),
('Etag', 'W/"%s"' % digest),
('Cache-Control', 'no-cache'),
])
return [body]
else:
def read_file(file_path):
buffer_size = 1024
with open(file_path, 'rb') as f:
while True:
result = f.read(buffer_size)
if len(result) == 0:
break
yield result
start_response('200 OK',
[('Content-Type', guess_type(basename(path))[0] or 'application/octet-stream'),
('Etag', 'W/"%s"' % digest),
('Cache-Control', 'no-cache'),
])
return read_file(path)
elif isdir(path):
body = self.directory_listing(url_path, path).encode()
start_response('200 OK', [
('Content-Type', 'text/html; charset=UTF-8'),
])
return [body]
start_response('404 NOT_FOUND', [])
return []
@staticmethod
def stream_hash(source: BinaryIO, bufsize=0x1000) -> bytes:
if bufsize <= 0:
raise ValueError("Buffer size must be greater than 0")
md5 = hashlib.md5()
while True:
buf = source.read(bufsize)
if len(buf) == 0:
break
md5.update(buf)
return md5.digest()
@staticmethod
def file_hash(filepath, bufsize=0x1000) -> bytes:
if bufsize <= 0:
raise ValueError("Buffer size must be greater than 0")
md5 = hashlib.md5()
with open(filepath, 'rb') as f:
while True:
buf = f.read(bufsize)
if len(buf) == 0:
break
md5.update(buf)
return md5.digest()
@staticmethod
def parse_etag(etag: str) -> Optional[str]:
if etag is None:
return
start = etag.find('"')
if start < 0:
return
end = etag.find('"', start + 1)
return etag[start + 1: end]
def compute_etag_and_digest(
self,
etag_header: str,
path: str,
stream_source: Callable[[], BinaryIO],
mtime_supplier: Callable[[], float]
) -> tuple[str, str]:
cache_result = self.cache.get(path)
_mtime: Optional[float] = None
def mtime() -> float:
nonlocal _mtime
if not _mtime:
_mtime = mtime_supplier()
return _mtime
if not cache_result or cache_result[1] < mtime():
with stream_source() as stream:
digest = Server.stream_hash(stream).hex()
self.cache[path] = digest, mtime()
else:
digest = cache_result[0]
etag = Server.parse_etag(etag_header)
return etag, digest
@staticmethod
def not_modified(start_response, digest: str, cache_control=('Cache-Control', 'no-cache')) -> []:
start_response('304 Not Modified', [
('Etag', f'W/"{digest}"'),
cache_control,
])
return []
@staticmethod
def directory_listing(path_info, path) -> str:
title = "Directory listing for %s" % path_info
result = "<!DOCTYPE html><html><head>"
result += "<meta http-equiv=\"Content-Type\" content=\"text/html; charset=utf-8\">"
result += "<title>" + title + "</title></head>"
result += "<body><h1>" + title + "</h1><hr>"
result += "<ul>"
if path_info != '/':
result += "<li><a href=\"../\"/>../</li>"
def ls(filter):
return (entry for entry in sorted(listdir(path)) if filter(join(path, entry)))
for entry in ls(isdir):
result += '<li><a href="' + entry + '/' + '"/>' + entry + '/' + '</li>'
for entry in ls(lambda entry: isfile(entry) and is_markdown(entry)):
result += '<li><a href="' + entry + '"/>' + entry + '</li>'
return result

View File

@@ -0,0 +1,13 @@
.markdown-body {
box-sizing: border-box;
min-width: 200px;
max-width: 980px;
margin: 0 auto;
padding: 45px;
}
@media (max-width: 767px) {
.markdown-body {
padding: 15px;
}
}

View File

@@ -0,0 +1,695 @@
@font-face {
font-family: octicons-link;
src: url(data:font/woff;charset=utf-8;base64,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) format('woff');
}
.markdown-body {
-ms-text-size-adjust: 100%;
-webkit-text-size-adjust: 100%;
line-height: 1.5;
color: #24292e;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Helvetica, Arial, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", "Segoe UI Symbol";
font-size: 16px;
line-height: 1.5;
word-wrap: break-word;
}
.markdown-body .pl-c {
color: #6a737d;
}
.markdown-body .pl-c1,
.markdown-body .pl-s .pl-v {
color: #005cc5;
}
.markdown-body .pl-e,
.markdown-body .pl-en {
color: #6f42c1;
}
.markdown-body .pl-smi,
.markdown-body .pl-s .pl-s1 {
color: #24292e;
}
.markdown-body .pl-ent {
color: #22863a;
}
.markdown-body .pl-k {
color: #d73a49;
}
.markdown-body .pl-s,
.markdown-body .pl-pds,
.markdown-body .pl-s .pl-pse .pl-s1,
.markdown-body .pl-sr,
.markdown-body .pl-sr .pl-cce,
.markdown-body .pl-sr .pl-sre,
.markdown-body .pl-sr .pl-sra {
color: #032f62;
}
.markdown-body .pl-v,
.markdown-body .pl-smw {
color: #e36209;
}
.markdown-body .pl-bu {
color: #b31d28;
}
.markdown-body .pl-ii {
color: #fafbfc;
background-color: #b31d28;
}
.markdown-body .pl-c2 {
color: #fafbfc;
background-color: #d73a49;
}
.markdown-body .pl-c2::before {
content: "^M";
}
.markdown-body .pl-sr .pl-cce {
font-weight: bold;
color: #22863a;
}
.markdown-body .pl-ml {
color: #735c0f;
}
.markdown-body .pl-mh,
.markdown-body .pl-mh .pl-en,
.markdown-body .pl-ms {
font-weight: bold;
color: #005cc5;
}
.markdown-body .pl-mi {
font-style: italic;
color: #24292e;
}
.markdown-body .pl-mb {
font-weight: bold;
color: #24292e;
}
.markdown-body .pl-md {
color: #b31d28;
background-color: #ffeef0;
}
.markdown-body .pl-mi1 {
color: #22863a;
background-color: #f0fff4;
}
.markdown-body .pl-mc {
color: #e36209;
background-color: #ffebda;
}
.markdown-body .pl-mi2 {
color: #f6f8fa;
background-color: #005cc5;
}
.markdown-body .pl-mdr {
font-weight: bold;
color: #6f42c1;
}
.markdown-body .pl-ba {
color: #586069;
}
.markdown-body .pl-sg {
color: #959da5;
}
.markdown-body .pl-corl {
text-decoration: underline;
color: #032f62;
}
.markdown-body .octicon {
display: inline-block;
vertical-align: text-top;
fill: currentColor;
}
.markdown-body a {
background-color: transparent;
}
.markdown-body a:active,
.markdown-body a:hover {
outline-width: 0;
}
.markdown-body strong {
font-weight: inherit;
}
.markdown-body strong {
font-weight: bolder;
}
.markdown-body h1 {
font-size: 2em;
margin: 0.67em 0;
}
.markdown-body img {
border-style: none;
}
.markdown-body code,
.markdown-body kbd,
.markdown-body pre {
font-family: monospace, monospace;
font-size: 1em;
}
.markdown-body hr {
box-sizing: content-box;
height: 0;
overflow: visible;
}
.markdown-body input {
font: inherit;
margin: 0;
}
.markdown-body input {
overflow: visible;
}
.markdown-body [type="checkbox"] {
box-sizing: border-box;
padding: 0;
}
.markdown-body * {
box-sizing: border-box;
}
.markdown-body input {
font-family: inherit;
font-size: inherit;
line-height: inherit;
}
.markdown-body a {
color: #0366d6;
text-decoration: none;
}
.markdown-body a:hover {
text-decoration: underline;
}
.markdown-body strong {
font-weight: 600;
}
.markdown-body hr {
height: 0;
margin: 15px 0;
overflow: hidden;
background: transparent;
border: 0;
border-bottom: 1px solid #dfe2e5;
}
.markdown-body hr::before {
display: table;
content: "";
}
.markdown-body hr::after {
display: table;
clear: both;
content: "";
}
.markdown-body table {
border-spacing: 0;
border-collapse: collapse;
}
.markdown-body td,
.markdown-body th {
padding: 0;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body h1 {
font-size: 32px;
font-weight: 600;
}
.markdown-body h2 {
font-size: 24px;
font-weight: 600;
}
.markdown-body h3 {
font-size: 20px;
font-weight: 600;
}
.markdown-body h4 {
font-size: 16px;
font-weight: 600;
}
.markdown-body h5 {
font-size: 14px;
font-weight: 600;
}
.markdown-body h6 {
font-size: 12px;
font-weight: 600;
}
.markdown-body p {
margin-top: 0;
margin-bottom: 10px;
}
.markdown-body blockquote {
margin: 0;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 0;
margin-top: 0;
margin-bottom: 0;
}
.markdown-body ol ol,
.markdown-body ul ol {
list-style-type: lower-roman;
}
.markdown-body ul ul ol,
.markdown-body ul ol ol,
.markdown-body ol ul ol,
.markdown-body ol ol ol {
list-style-type: lower-alpha;
}
.markdown-body dd {
margin-left: 0;
}
.markdown-body code {
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
}
.markdown-body pre {
margin-top: 0;
margin-bottom: 0;
font-family: "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
font-size: 12px;
}
.markdown-body .octicon {
vertical-align: text-bottom;
}
.markdown-body .pl-0 {
padding-left: 0 !important;
}
.markdown-body .pl-1 {
padding-left: 4px !important;
}
.markdown-body .pl-2 {
padding-left: 8px !important;
}
.markdown-body .pl-3 {
padding-left: 16px !important;
}
.markdown-body .pl-4 {
padding-left: 24px !important;
}
.markdown-body .pl-5 {
padding-left: 32px !important;
}
.markdown-body .pl-6 {
padding-left: 40px !important;
}
.markdown-body::before {
display: table;
content: "";
}
.markdown-body::after {
display: table;
clear: both;
content: "";
}
.markdown-body>*:first-child {
margin-top: 0 !important;
}
.markdown-body>*:last-child {
margin-bottom: 0 !important;
}
.markdown-body a:not([href]) {
color: inherit;
text-decoration: none;
}
.markdown-body .anchor {
float: left;
padding-right: 4px;
margin-left: -20px;
line-height: 1;
}
.markdown-body .anchor:focus {
outline: none;
}
.markdown-body p,
.markdown-body blockquote,
.markdown-body ul,
.markdown-body ol,
.markdown-body dl,
.markdown-body table,
.markdown-body pre {
margin-top: 0;
margin-bottom: 16px;
}
.markdown-body hr {
height: 0.25em;
padding: 0;
margin: 24px 0;
background-color: #e1e4e8;
border: 0;
}
.markdown-body blockquote {
padding: 0 1em;
color: #6a737d;
border-left: 0.25em solid #dfe2e5;
}
.markdown-body blockquote>:first-child {
margin-top: 0;
}
.markdown-body blockquote>:last-child {
margin-bottom: 0;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font-size: 11px;
line-height: 10px;
color: #444d56;
vertical-align: middle;
background-color: #fafbfc;
border: solid 1px #c6cbd1;
border-bottom-color: #959da5;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #959da5;
}
.markdown-body h1,
.markdown-body h2,
.markdown-body h3,
.markdown-body h4,
.markdown-body h5,
.markdown-body h6 {
margin-top: 24px;
margin-bottom: 16px;
font-weight: 600;
line-height: 1.25;
}
.markdown-body h1 .octicon-link,
.markdown-body h2 .octicon-link,
.markdown-body h3 .octicon-link,
.markdown-body h4 .octicon-link,
.markdown-body h5 .octicon-link,
.markdown-body h6 .octicon-link {
color: #1b1f23;
vertical-align: middle;
visibility: hidden;
}
.markdown-body h1:hover .anchor,
.markdown-body h2:hover .anchor,
.markdown-body h3:hover .anchor,
.markdown-body h4:hover .anchor,
.markdown-body h5:hover .anchor,
.markdown-body h6:hover .anchor {
text-decoration: none;
}
.markdown-body h1:hover .anchor .octicon-link,
.markdown-body h2:hover .anchor .octicon-link,
.markdown-body h3:hover .anchor .octicon-link,
.markdown-body h4:hover .anchor .octicon-link,
.markdown-body h5:hover .anchor .octicon-link,
.markdown-body h6:hover .anchor .octicon-link {
visibility: visible;
}
.markdown-body h1 {
padding-bottom: 0.3em;
font-size: 2em;
border-bottom: 1px solid #eaecef;
}
.markdown-body h2 {
padding-bottom: 0.3em;
font-size: 1.5em;
border-bottom: 1px solid #eaecef;
}
.markdown-body h3 {
font-size: 1.25em;
}
.markdown-body h4 {
font-size: 1em;
}
.markdown-body h5 {
font-size: 0.875em;
}
.markdown-body h6 {
font-size: 0.85em;
color: #6a737d;
}
.markdown-body ul,
.markdown-body ol {
padding-left: 2em;
}
.markdown-body ul ul,
.markdown-body ul ol,
.markdown-body ol ol,
.markdown-body ol ul {
margin-top: 0;
margin-bottom: 0;
}
.markdown-body li {
word-wrap: break-all;
}
.markdown-body li>p {
margin-top: 16px;
}
.markdown-body li+li {
margin-top: 0.25em;
}
.markdown-body dl {
padding: 0;
}
.markdown-body dl dt {
padding: 0;
margin-top: 16px;
font-size: 1em;
font-style: italic;
font-weight: 600;
}
.markdown-body dl dd {
padding: 0 16px;
margin-bottom: 16px;
}
.markdown-body table {
display: block;
width: 100%;
overflow: auto;
}
.markdown-body table th {
font-weight: 600;
}
.markdown-body table th,
.markdown-body table td {
padding: 6px 13px;
border: 1px solid #dfe2e5;
}
.markdown-body table tr {
background-color: #fff;
border-top: 1px solid #c6cbd1;
}
.markdown-body table tr:nth-child(2n) {
background-color: #f6f8fa;
}
.markdown-body img {
max-width: 100%;
box-sizing: content-box;
background-color: #fff;
}
.markdown-body img[align=right] {
padding-left: 20px;
}
.markdown-body img[align=left] {
padding-right: 20px;
}
.markdown-body code {
padding: 0.2em 0.4em;
margin: 0;
font-size: 85%;
background-color: rgba(27,31,35,0.05);
border-radius: 3px;
}
.markdown-body pre {
word-wrap: normal;
}
.markdown-body pre>code {
padding: 0;
margin: 0;
font-size: 100%;
word-break: normal;
white-space: pre;
background: transparent;
border: 0;
}
.markdown-body .highlight {
margin-bottom: 16px;
}
.markdown-body .highlight pre {
margin-bottom: 0;
word-break: normal;
}
.markdown-body .highlight pre,
.markdown-body pre {
padding: 16px;
overflow: auto;
font-size: 85%;
line-height: 1.45;
background-color: #f6f8fa;
border-radius: 3px;
}
.markdown-body pre code {
display: inline;
max-width: auto;
padding: 0;
margin: 0;
overflow: visible;
line-height: inherit;
word-wrap: normal;
background-color: transparent;
border: 0;
}
.markdown-body .full-commit .btn-outline:not(:disabled):hover {
color: #005cc5;
border-color: #005cc5;
}
.markdown-body kbd {
display: inline-block;
padding: 3px 5px;
font: 11px "SFMono-Regular", Consolas, "Liberation Mono", Menlo, Courier, monospace;
line-height: 10px;
color: #444d56;
vertical-align: middle;
background-color: #fafbfc;
border: solid 1px #d1d5da;
border-bottom-color: #c6cbd1;
border-radius: 3px;
box-shadow: inset 0 -1px 0 #c6cbd1;
}
.markdown-body :checked+.radio-label {
position: relative;
z-index: 1;
border-color: #0366d6;
}
.markdown-body .task-list-item {
list-style-type: none;
}
.markdown-body .task-list-item+.task-list-item {
margin-top: 3px;
}
.markdown-body .task-list-item input {
margin: 0 0.2em 0.25em -1.6em;
vertical-align: middle;
}
.markdown-body hr {
border-bottom-color: #eee;
}

View File

@@ -0,0 +1,19 @@
function req(first) {
var xmlhttp = new XMLHttpRequest();
xmlhttp.onload = function() {
if (xmlhttp.status == 200) {
document.querySelector("article.markdown-body").innerHTML = xmlhttp.responseText;
} else if(xmlhttp.status == 304) {
} else {
console.log(xmlhttp.status, xmlhttp.statusText);
}
req(false);
};
xmlhttp.onerror = function() {
console.log(xmlhttp.status, xmlhttp.statusText);
setTimeout(req, 1000, false);
};
xmlhttp.open("GET", location.pathname + "?reload", true);
xmlhttp.send();
}
req(true);

View File

@@ -0,0 +1,75 @@
pre { line-height: 125%; }
td.linenos .normal { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
span.linenos { color: inherit; background-color: transparent; padding-left: 5px; padding-right: 5px; }
td.linenos .special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
span.linenos.special { color: #000000; background-color: #ffffc0; padding-left: 5px; padding-right: 5px; }
.codehilite .hll { background-color: #ffffcc }
.codehilite { background: #f8f8f8; }
.codehilite .c { color: #3D7B7B; font-style: italic } /* Comment */
.codehilite .err { border: 1px solid #FF0000 } /* Error */
.codehilite .k { color: #008000; font-weight: bold } /* Keyword */
.codehilite .o { color: #666666 } /* Operator */
.codehilite .ch { color: #3D7B7B; font-style: italic } /* Comment.Hashbang */
.codehilite .cm { color: #3D7B7B; font-style: italic } /* Comment.Multiline */
.codehilite .cp { color: #9C6500 } /* Comment.Preproc */
.codehilite .cpf { color: #3D7B7B; font-style: italic } /* Comment.PreprocFile */
.codehilite .c1 { color: #3D7B7B; font-style: italic } /* Comment.Single */
.codehilite .cs { color: #3D7B7B; font-style: italic } /* Comment.Special */
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
.codehilite .ge { font-style: italic } /* Generic.Emph */
.codehilite .ges { font-weight: bold; font-style: italic } /* Generic.EmphStrong */
.codehilite .gr { color: #E40000 } /* Generic.Error */
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
.codehilite .gi { color: #008400 } /* Generic.Inserted */
.codehilite .go { color: #717171 } /* Generic.Output */
.codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
.codehilite .gs { font-weight: bold } /* Generic.Strong */
.codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
.codehilite .gt { color: #0044DD } /* Generic.Traceback */
.codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
.codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
.codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
.codehilite .kp { color: #008000 } /* Keyword.Pseudo */
.codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
.codehilite .kt { color: #B00040 } /* Keyword.Type */
.codehilite .m { color: #666666 } /* Literal.Number */
.codehilite .s { color: #BA2121 } /* Literal.String */
.codehilite .na { color: #687822 } /* Name.Attribute */
.codehilite .nb { color: #008000 } /* Name.Builtin */
.codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
.codehilite .no { color: #880000 } /* Name.Constant */
.codehilite .nd { color: #AA22FF } /* Name.Decorator */
.codehilite .ni { color: #717171; font-weight: bold } /* Name.Entity */
.codehilite .ne { color: #CB3F38; font-weight: bold } /* Name.Exception */
.codehilite .nf { color: #0000FF } /* Name.Function */
.codehilite .nl { color: #767600 } /* Name.Label */
.codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
.codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
.codehilite .nv { color: #19177C } /* Name.Variable */
.codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
.codehilite .w { color: #bbbbbb } /* Text.Whitespace */
.codehilite .mb { color: #666666 } /* Literal.Number.Bin */
.codehilite .mf { color: #666666 } /* Literal.Number.Float */
.codehilite .mh { color: #666666 } /* Literal.Number.Hex */
.codehilite .mi { color: #666666 } /* Literal.Number.Integer */
.codehilite .mo { color: #666666 } /* Literal.Number.Oct */
.codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
.codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
.codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
.codehilite .se { color: #AA5D1F; font-weight: bold } /* Literal.String.Escape */
.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
.codehilite .si { color: #A45A77; font-weight: bold } /* Literal.String.Interpol */
.codehilite .sx { color: #008000 } /* Literal.String.Other */
.codehilite .sr { color: #A45A77 } /* Literal.String.Regex */
.codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
.codehilite .ss { color: #19177C } /* Literal.String.Symbol */
.codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
.codehilite .fm { color: #0000FF } /* Name.Function.Magic */
.codehilite .vc { color: #19177C } /* Name.Variable.Class */
.codehilite .vg { color: #19177C } /* Name.Variable.Global */
.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
.codehilite .vm { color: #19177C } /* Name.Variable.Magic */
.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */

View File

@@ -0,0 +1,13 @@
<!DOCTYPE html>
<html>
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
{css}
{script}
</head>
<body>
<article class="markdown-body">
{content}
</article>
</body>
</html>

View File

@@ -0,0 +1,25 @@
import logging
from .server import Server
from uwsgi import log
class UwsgiHandler(logging.Handler):
def emit(self, record: logging.LogRecord) -> None:
log(self.formatter.format(record))
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s [%(threadName)s] (%(name)s) %(levelname)s %(message)s',
handlers=[UwsgiHandler()]
)
server = Server()
def application(env, start_response):
return server.handle_request(
env['REQUEST_METHOD'],
env['PATH_INFO'],
env.get('HTTP_IF_NONE_MATCH', None),
env.get('QUERY_STRING', None),
start_response
)