lepa_octomode/octomode.py
2026-02-01 22:22:58 +01:00

323 lines
10 KiB
Python
Executable file

import os
import json
from flask import Flask, request, render_template, redirect, url_for
from urllib.request import urlopen
from urllib.parse import urlencode
import html
import re
# To sanitize Flask input fields
from markupsafe import Markup, escape
# To sanitize Markdown input
import pypandoc
# import bleach
# To read the Markdown metadat
import markdown
from filter_registry import pad_content_filters
import pad_filters
APP = Flask(__name__)
APP.config.from_pyfile('settings.py')
# ---
# My attempt as showing helpful error messages.
# the class holds an error string to show in the error.html tempalte
# for now mostly for when the space in the yaml meta is missing.
class MarkdownRenderError(RuntimeError):
def __init__(self, kind, message, original):
super().__init__(message)
self.kind = kind
self.original = original
def get_pad_content(pad_name, ext=""):
if ext:
pad_name = f'{ pad_name }{ ext }'
# print(pad_name)
arguments = {
'padID' : pad_name,
'apikey' : APP.config['PAD_API_KEY']
}
api_call = 'getText'
response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode()))
# create pad in case it does not yet exist
if response['code'] == 1 and 'padID does not exist' == response['message']:
api_call = 'createPad'
urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode())
api_call = 'getText'
response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode()))
content = response['data']['text']
# print("before: " + content)
# print( "GET PAD CONTENT" )
for f in pad_content_filters:
content = f(content)
# print("after: " + content)
return content
def all_pads():
arguments = {
'apikey' : APP.config['PAD_API_KEY'],
}
api_call = 'listAllPads'
response = json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode()))
return response
# get all pads that end in .md so we can show them on the frontpage
def all_publications():
pads = all_pads()
pubs = []
if(pads and pads['data'] and pads['data']['padIDs']):
for pad in pads['data']['padIDs']:
if pad.endswith('.md'):
name = pad.removesuffix('.md')
pubs.append(name)
return pubs
else:
return []
def create_pad_on_first_run(name, ext):
pads = all_pads()
pad = name+ext
if pad not in pads['data']['padIDs']:
# Select default template
if 'md' in ext:
default_template = 'templates/default.md'
elif 'css' in ext:
default_template = 'templates/default.css'
default_template = open(default_template).read()
# Create pad and add the default template
arguments = {
'padID' : pad,
'apikey' : APP.config['PAD_API_KEY'],
'text' : default_template
}
api_call = 'createPad'
json.load(urlopen(f"{ APP.config['PAD_API_URL'] }/{ api_call }", data=urlencode(arguments).encode()))
def md_to_html(md_pad_content):
# Convert Markdown to HTML
# html = markdown.markdown(md_pad_content, extensions=['meta', 'attr_list']) # attr_list does not work
# missing a sapce in the metadata block causes a crash,
# try to show a helpful error message...
# Maybe, we could fix the yaml instead?
try:
html = pypandoc.convert_text(md_pad_content, 'html', format='md')
except RuntimeError as exc:
message = str(exc)
if "YAML" in message or "YAML metadata" in message:
raise MarkdownRenderError(
kind="yaml",
message="Invalid YAML metadata. Use `key: value` and add a blank line.",
original=str(exc)
) from exc
raise MarkdownRenderError(
kind="markdown",
message = "Markdown conversion failed. Please check the markdown for errors.",
original = str(exc)
) from exc
# Sanitize the Markdown
# html = bleach.clean(html)
# Another built-in Flask way to sanitize
# html = escape(html)
html = Markup(html)
return html
def get_md_metadata(md_pad_content):
# Read the metadata from the Markdown
md = markdown.Markdown(extensions=['meta'])
md.convert(md_pad_content)
metadata = md.Meta
return metadata
def get_app_root():
if APP.config['APPLICATION_ROOT'] == '/':
app_root = ''
elif APP.config['APPLICATION_ROOT'].endswith('/'):
app_root = APP.config['APPLICATION_ROOT'][:-1]
else:
app_root = APP.config['APPLICATION_ROOT']
return app_root
def get_meta(metadata, key, default=None):
return metadata.get(key, [default])[0]
def render_markdown_error(name, error):
print("ERROR", error)
return render_template(
'error.html',
name=name.strip(),
error_text=str(error),
error=error,
lang="en",
title="Markdown error"
), 400
def clean_string(input_string):
input_string = input_string.lower()
snake_case_string = re.sub(r'[\s!]+', '_', input_string)
return snake_case_string.strip('_')
@APP.template_filter('prettify') # use it in a template with | prettify
def prettify_string(input_string):
space_string = input_string.replace("_", " ")
return space_string.title()
# ---
@APP.route('/', methods=['GET', 'POST'])
def index():
name = False
if request.values.get('name'):
name = escape(request.values.get('name')) # Returns a Markup() object, which is "None" when False
if name:
# This is when the environment is "created"
# The pads are filled with the default templates (pad, stylesheet, template)
exts = ['.md', '.css']
# clean up the name so we dont get x number of different publications when people mistype
name = clean_string(name)
for ext in exts:
create_pad_on_first_run(name, ext)
return redirect(url_for("pad", name=name))
else:
pubs = all_publications()
return render_template('start.html', pubs = pubs, home_pad_url=APP.config['HOME_PAD_URL'])
@APP.route('/<name>/')
def main(name):
return redirect(url_for("pad", name=name))
@APP.route('/<name>/pad/')
def pad(name):
url = f"{ APP.config['PAD_URL'] }/{ name }.md"
return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL'])
@APP.route('/<name>/stylesheet/')
def stylesheet(name):
url = f"{ APP.config['PAD_URL'] }/{ name }.css"
return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL'])
@APP.route('/<name>/html/')
def html(name):
app_root = get_app_root()
url = f"{ app_root }/{ name }/preview.html"
return render_template('iframe.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL'])
@APP.route('/<name>/pdf/')
def pdf(name):
app_root = get_app_root()
pagedjs_base = f"{ app_root }/{name}/pagedjs.html"
impose = request.args.get("impose") == "1"
single_page = request.args.get("single") == "1"
params = {}
if impose:
params["impose"] = "1"
if single_page:
params["single"] = "1"
query = f"?{ urlencode(params) }" if params else ""
url = f"{ pagedjs_base }{ query }"
return render_template('pdf.html', url=url, pagedjs_base=pagedjs_base, name=name.strip(), pad_url=APP.config['PAD_URL'])
# @APP.route('/<name>/impose/')
# def impose(name):
# app_root = get_app_root()
# url = f"{ app_root }/{name}/imposed.html"
# return render_template('pdf.html', url=url, name=name.strip(), pad_url=APP.config['PAD_URL'])
# //////////////////
# RENDERED RESOURCES
# //////////////////
# (These are not saved as a file on the server)
@APP.route('/<name>/stylesheet.css')
def css(name):
css = get_pad_content(name, '.css')
# Insert CSS sanitizer here.
return css, 200, {'Content-Type': 'text/css; charset=utf-8'}
@APP.route('/<name>/preview.html')
def preview(name):
# TO GENERATE THE PREVIEW WEBPAGE
md_pad_content = get_pad_content(name, ext='.md')
try:
html = md_to_html(md_pad_content)
except MarkdownRenderError as exc:
return render_markdown_error(name, exc)
metadata = get_md_metadata(md_pad_content)
lang = get_meta(metadata, 'language', 'en')
title = get_meta(metadata, 'title', 'Untitled')
return render_template('preview.html', name=name.strip(), pad_content=html, lang=lang, title=title)
@APP.route('/<name>/pagedjs.html')
def pagedjs(name):
# TO GENERATE THE PAGED.JS WEBPAGE
md_pad_content = get_pad_content(name, ext='.md')
try:
html = md_to_html(md_pad_content)
except MarkdownRenderError as exc:
return render_markdown_error(name, exc)
metadata = get_md_metadata(md_pad_content)
lang = get_meta(metadata, 'language', 'en')
title = get_meta(metadata, 'title', 'Untitled')
cover = get_meta(metadata, 'cover', None)
impose = request.args.get("impose") == "1"
single_page = request.args.get("single") == "1"
return render_template(
'pagedjs.html',
name=name.strip(),
pad_content=html,
lang=lang,
title=title,
cover=cover,
impose=impose,
single_page=single_page
)
# @APP.route('/<name>/imposed.html')
# def imposed(name):
# # TO GENERATE THE IMPOSED WEBPAGE
# md_pad_content = get_pad_content(name, ext='.md')
# try:
# html = md_to_html(md_pad_content)
# except MarkdownRenderError as exc:
# return render_markdown_error(name, exc)
# metadata = get_md_metadata(md_pad_content)
# lang = get_meta(metadata, 'language', 'en')
# title = get_meta(metadata, 'title', 'Untitled')
# cover = get_meta(metadata, 'cover', None)
# impose = True #request.args.get("impose") == "true"
# return render_template('pagedjs.html', name=name.strip(), pad_content=html, lang=lang, title=title, cover=cover, impose=impose)
# //////////////////
if __name__ == '__main__':
APP.debug = True
APP.env = "development"
# APP.debug = False
# APP.env = "production"
APP.run(host="0.0.0.0", port=APP.config["PORTNUMBER"], threaded=True)