Browse Source
* Readme is now relevant * Pandoc converts everything in content/ and puts it in webResources * Blog server can render pandoc output to a post template (though my browser isn't formatting it correctly...)master

8 changed files with 284 additions and 0 deletions
@ -0,0 +1,130 @@ |
|||
import os |
|||
import subprocess |
|||
|
|||
contentDirectory = "content" |
|||
renderedDirectory = "webResources" |
|||
|
|||
# Pairs of extension, pandoc read type |
|||
convertableContentTypes = [(".org", "org")] |
|||
contentExtensions = [] |
|||
for contentType in convertableContentTypes: |
|||
contentExtensions.append(contentType[0]) |
|||
contentExtensions = tuple(contentExtensions) |
|||
|
|||
renderedContentTypes = [(".html", "html")] |
|||
defaultRenderedTypeIndex = 0 |
|||
renderedContentExtensions = [] |
|||
for contentType in renderedContentTypes: |
|||
renderedContentExtensions.append(contentType[0]) |
|||
renderedContentExtensions = tuple(renderedContentExtensions) |
|||
|
|||
def getContentReadType(contentFilename): |
|||
contentExtension = contentFilename[contentFilename.rfind("."):] |
|||
for contentType in convertableContentTypes: |
|||
if contentExtension == contentType[0]: |
|||
return contentType[1] |
|||
|
|||
# Strip the content directory |
|||
def getContentLocalName(contentFilename): |
|||
return contentFilename[len(contentDirectory):] |
|||
|
|||
# Strip the rendered directory |
|||
def getRenderedLocalName(filename): |
|||
return filename[len(renderedDirectory):] |
|||
|
|||
def stripExtension(filename): |
|||
return filename[:filename.rfind(".")] |
|||
|
|||
def contentFilenameToRenderedFilename(contentFilename): |
|||
return "{}{}{}".format(renderedDirectory, |
|||
stripExtension(getContentLocalName(contentFilename)), |
|||
renderedContentTypes[defaultRenderedTypeIndex][0]) |
|||
|
|||
def renderContent(contentFilename): |
|||
# Content modified recently; render it |
|||
print("\tRendering {}".format(contentFilename)) |
|||
|
|||
outputFilename = contentFilenameToRenderedFilename(contentFilename) |
|||
|
|||
# TODO: Support multiple output formats? |
|||
subprocess.run(["pandoc", |
|||
"-r", getContentReadType(contentFilename), |
|||
"-w", renderedContentTypes[defaultRenderedTypeIndex][1], |
|||
"-o", outputFilename, contentFilename]) |
|||
|
|||
# List of all content files |
|||
contentCache = [] |
|||
|
|||
renderedCache = [] |
|||
|
|||
renderedDictionary = {} |
|||
|
|||
def updateRenderedCache(): |
|||
global renderedCache |
|||
renderedCache = [] |
|||
# Get all rendered files |
|||
for root, dirs, files in os.walk(renderedDirectory): |
|||
for file in files: |
|||
if file.endswith(renderedContentExtensions): |
|||
renderedFile = os.path.join(root, file) |
|||
renderedCache.append(renderedFile) |
|||
# The path actually used to look up the content (strip '/' from front) |
|||
contentPath = getRenderedLocalName(stripExtension(renderedFile))[1:] |
|||
print("\t'{}' = '{}'".format(contentPath, renderedFile)) |
|||
# No use for the value yet, we just want fast key lookups |
|||
renderedDictionary[contentPath] = renderedFile |
|||
|
|||
def getRenderedBody(contentPath): |
|||
body = None |
|||
if contentPath in renderedDictionary: |
|||
renderedFilename = renderedDictionary[contentPath] |
|||
renderedFile = open(renderedFilename) |
|||
body = renderedFile.readlines() |
|||
body = "".join(body) |
|||
renderedFile.close() |
|||
return body |
|||
|
|||
def checkForContentChange(): |
|||
global contentCache |
|||
|
|||
print("Checking for content updates...") |
|||
|
|||
# Get all content files |
|||
contentCache = [] |
|||
for root, dirs, files in os.walk(contentDirectory): |
|||
for file in files: |
|||
if file.endswith(contentExtensions): |
|||
contentCache.append(os.path.join(root, file)) |
|||
|
|||
updateRenderedCache() |
|||
|
|||
numRenderedFiles = 0 |
|||
|
|||
# Compare timestamps to determine which files need regeneration |
|||
for contentFilename in contentCache: |
|||
contentModified = os.path.getmtime(contentFilename) |
|||
contentLocalName = getContentLocalName(contentFilename) |
|||
|
|||
renderedFileFound = False |
|||
for renderedFile in renderedCache: |
|||
if stripExtension(renderedFile[len(renderedDirectory):]) != stripExtension(contentLocalName): |
|||
continue |
|||
|
|||
renderedFileFound = True |
|||
renderedModified = os.path.getmtime(renderedFile) |
|||
|
|||
# Modified content |
|||
if contentModified > renderedModified: |
|||
renderContent(contentFilename) |
|||
numRenderedFiles += 1 |
|||
|
|||
# New content file |
|||
if not renderedFileFound: |
|||
renderContent(contentFilename) |
|||
numRenderedFiles += 1 |
|||
|
|||
# We generated new content; make sure the cache is up-to-date |
|||
if numRenderedFiles: |
|||
updateRenderedCache() |
|||
|
|||
print("Checking for content updates done. Rendered {} files".format(numRenderedFiles)) |
@ -0,0 +1,3 @@ |
|||
#!/bin/bash |
|||
mkdir certificates |
|||
openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout certificates/server_jupyter_based.crt.key -out certificates/server_jupyter_based.crt.pem |
@ -0,0 +1,103 @@ |
|||
#!/usr/bin/env python |
|||
|
|||
import tornado.ioloop |
|||
import tornado.web |
|||
import tornado.websocket |
|||
import tornado.httpclient |
|||
import tornado.httpserver |
|||
import tornado.gen |
|||
|
|||
import os |
|||
from datetime import datetime |
|||
|
|||
import ContentConverter |
|||
|
|||
# |
|||
# Tornado handlers |
|||
# |
|||
|
|||
class HomeHandler(tornado.web.RequestHandler): |
|||
def get(self): |
|||
self.write('''<html><head><link rel="stylesheet" type="text/css" href="webResources/styles.css"></head> |
|||
<body><p>{}</p></body> |
|||
</html>'''.format("Hello, world!")) |
|||
|
|||
class BlogHandler(tornado.web.RequestHandler): |
|||
def get(self, request): |
|||
contentTitle = "Blog: " + request |
|||
renderedBody = ContentConverter.getRenderedBody(request) |
|||
if not renderedBody: |
|||
renderedBody = "<p>The post under '{}' does not exist.</p>".format(request) |
|||
|
|||
self.render("templates/BlogPost.html", title=contentTitle, postBody=renderedBody) |
|||
|
|||
# |
|||
# Startup |
|||
# |
|||
|
|||
def make_app(): |
|||
return tornado.web.Application([ |
|||
# Home page |
|||
(r'/', HomeHandler), |
|||
|
|||
(r'/blog/(.*)', BlogHandler), |
|||
|
|||
# # Handles messages for run script |
|||
# (r'/runScriptWebSocket', RunScriptWebSocket), |
|||
|
|||
# Upload handler |
|||
# (r'/upload', UploadHandler), |
|||
|
|||
# # Don't change this 'output' here without changing the other places as well |
|||
# (r'/output/(.*)', tornado.web.StaticFileHandler, {'path' : 'output'}), |
|||
|
|||
# Static files. Keep this at the bottom because it handles everything else |
|||
# TODO put these in a subdir so everything isn't accessible |
|||
(r'/webResources/(.*)', tornado.web.StaticFileHandler, {'path' : 'webResources'}), |
|||
], |
|||
xsrf_cookies=True, |
|||
cookie_secret='this is my org blog') |
|||
|
|||
if __name__ == '__main__': |
|||
|
|||
# Before startup, convert anything which needs to be converted |
|||
ContentConverter.checkForContentChange() |
|||
|
|||
port = 8888 |
|||
print('\nStarting Simple Org Blog Server on port {}...'.format(port)) |
|||
app = make_app() |
|||
|
|||
# |
|||
# Notes on SSL |
|||
# |
|||
# Certificate generation (for localhost) (didn't actually work): |
|||
# https://medium.freecodecamp.org/how-to-get-https-working-on-your-local-development-environment-in-5-minutes-7af615770eec?gi=bd966500e56a |
|||
# Tornado instructions: |
|||
# https://stackoverflow.com/questions/18307131/how-to-create-https-tornado-server |
|||
# Note that I added the rootCA to Certificates trust in Firefox Preferences as well (didn't do anything) |
|||
# |
|||
# What I actually did: |
|||
# openssl req -x509 -nodes -days 365 -newkey rsa:1024 -keyout certificates/server_jupyter_based.crt.key -out certificates/server_jupyter_based.crt.pem |
|||
# (from https://jupyter-notebook.readthedocs.io/en/latest/public_server.html) |
|||
# I then had to tell Firefox to trust this certificate even though it is self-signing (because |
|||
# I want a free certificate for this non-serious project) |
|||
useSSL = True |
|||
if useSSL: |
|||
app.listen(port, ssl_options={"certfile":"certificates/server_jupyter_based.crt.pem", |
|||
"keyfile":"certificates/server_jupyter_based.crt.key"}) |
|||
else: |
|||
# Show the warning only if SSL is not enabled |
|||
print('\n\tWARNING: Do NOT run this server on the internet (e.g. port-forwarded)' |
|||
' nor when\n\t connected to an insecure LAN! It is not protected against malicious use.\n') |
|||
|
|||
app.listen(port) |
|||
|
|||
ioLoop = tornado.ioloop.IOLoop.current() |
|||
|
|||
# Periodically check to see if anything has changed which needs to be converted to .html |
|||
# Check every fifteen minutes |
|||
checkForContentChangeCallback = tornado.ioloop.PeriodicCallback(ContentConverter.checkForContentChange, |
|||
15 * 60 * 1000) |
|||
checkForContentChangeCallback.start() |
|||
|
|||
ioLoop.start() |
@ -0,0 +1,6 @@ |
|||
* Test post |
|||
This is a test post. |
|||
|
|||
This is an update. |
|||
|
|||
Here's another update |
@ -0,0 +1,9 @@ |
|||
<html> |
|||
<head> |
|||
<title>{{ title }}</title> |
|||
<link rel="stylesheet" type="text/css" href="webResources/styles.css"> |
|||
</head> |
|||
<body> |
|||
{{ postBody }} |
|||
</body> |
|||
</html> |
@ -0,0 +1,4 @@ |
|||
<h1 id="test-post">Test post</h1> |
|||
<p>This is a test post.</p> |
|||
<p>This is an update.</p> |
|||
<p>Here's another update</p> |
@ -0,0 +1,29 @@ |
|||
body { |
|||
margin: 0 auto; |
|||
width: 80%; |
|||
height: 100%; |
|||
background-color: #333333; |
|||
max-width: 800px; |
|||
} |
|||
|
|||
p, |
|||
h1, |
|||
h2, |
|||
h3, |
|||
a, |
|||
li { |
|||
color: #cccccc; |
|||
} |
|||
|
|||
label { |
|||
font-size: large; |
|||
color: #cccccc; |
|||
} |
|||
|
|||
li { |
|||
margin-top: 0px; |
|||
margin-bottom: 0px; |
|||
margin-left: 20px; |
|||
font-size: small; |
|||
color: #aaaaaa; |
|||
} |
Loading…
Reference in new issue