Add Markdown support to blogs, comments, pages
This commit is contained in:
parent
b58563b0d1
commit
978928c97e
|
@ -4,3 +4,5 @@ redis
|
|||
bcrypt
|
||||
pillow
|
||||
requests
|
||||
Markdown
|
||||
Pygments
|
|
@ -120,10 +120,21 @@ def catchall(path):
|
|||
abspath = os.path.abspath("{}/{}".format(root, path))
|
||||
if os.path.isfile(abspath):
|
||||
return send_file(abspath)
|
||||
elif not "." in path and os.path.isfile(abspath + ".html"):
|
||||
return rophako.utils.template(path + ".html")
|
||||
elif not "." in path and os.path.isfile(abspath + "/index.html"):
|
||||
return rophako.utils.template(path + "/index.html")
|
||||
|
||||
# The exact file wasn't found, look for some extensions and index pages.
|
||||
suffixes = [
|
||||
".html",
|
||||
"/index.html",
|
||||
".md", # Markdown formatted pages.
|
||||
"/index.md",
|
||||
]
|
||||
for suffix in suffixes:
|
||||
if not "." in path and os.path.isfile(abspath + suffix):
|
||||
# HTML, or Markdown?
|
||||
if suffix.endswith(".html"):
|
||||
return rophako.utils.template(path + suffix)
|
||||
else:
|
||||
return rophako.utils.markdown_template(abspath + suffix)
|
||||
|
||||
return not_found("404")
|
||||
|
||||
|
|
|
@ -73,11 +73,15 @@ def get_entry(post_id):
|
|||
if len(db["fid"]) == 0:
|
||||
db["fid"] = str(post_id)
|
||||
|
||||
# If no "format" option, set it to HTML (legacy)
|
||||
if db.get("format", "") == "":
|
||||
db["format"] = "html"
|
||||
|
||||
return db
|
||||
|
||||
|
||||
def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
|
||||
privacy, ip, emoticons, comments, body):
|
||||
privacy, ip, emoticons, comments, format, body):
|
||||
"""Post (or update) a blog entry."""
|
||||
|
||||
# Fetch the index.
|
||||
|
@ -139,6 +143,7 @@ def post_entry(post_id, fid, epoch, author, subject, avatar, categories,
|
|||
privacy = privacy or "public",
|
||||
author = author,
|
||||
subject = subject,
|
||||
format = format,
|
||||
body = body,
|
||||
))
|
||||
|
||||
|
|
|
@ -13,7 +13,7 @@ import config
|
|||
import rophako.jsondb as JsonDB
|
||||
import rophako.model.user as User
|
||||
import rophako.model.emoticons as Emoticons
|
||||
from rophako.utils import send_email
|
||||
from rophako.utils import send_email, render_markdown
|
||||
from rophako.log import logger
|
||||
|
||||
|
||||
|
@ -162,15 +162,12 @@ def unsubscribe(thread, email):
|
|||
|
||||
def format_message(message):
|
||||
"""HTML sanitize the message and format it for display."""
|
||||
# We basically want to escape HTML symbols (like what Flask does for us
|
||||
# automatically), but we want line breaks to translate to literal <br> tags.
|
||||
message = re.sub(r'&', '&', message)
|
||||
message = re.sub(r'<', '<', message)
|
||||
message = re.sub(r'>', '>', message)
|
||||
message = re.sub(r'"', '"', message)
|
||||
message = re.sub(r"'", ''', message)
|
||||
message = re.sub(r'\n', '<br>', message)
|
||||
message = re.sub(r'\r', '', message)
|
||||
|
||||
# Comments use Markdown formatting, and HTML tags are escaped by default.
|
||||
message = render_markdown(message)
|
||||
|
||||
# Don't allow commenters to use images.
|
||||
message = re.sub(r'<img.+?/>', '', message)
|
||||
|
||||
# Process emoticons.
|
||||
message = Emoticons.render(message)
|
||||
|
|
|
@ -13,7 +13,7 @@ import rophako.model.user as User
|
|||
import rophako.model.blog as Blog
|
||||
import rophako.model.comment as Comment
|
||||
import rophako.model.emoticons as Emoticons
|
||||
from rophako.utils import template, pretty_time, login_required
|
||||
from rophako.utils import template, render_markdown, pretty_time, login_required
|
||||
from rophako.log import logger
|
||||
from config import *
|
||||
|
||||
|
@ -44,9 +44,15 @@ def entry(fid):
|
|||
post = Blog.get_entry(post_id)
|
||||
post["post_id"] = post_id
|
||||
|
||||
# Render the body.
|
||||
if post["format"] == "markdown":
|
||||
post["rendered_body"] = render_markdown(post["body"])
|
||||
else:
|
||||
post["rendered_body"] = post["body"]
|
||||
|
||||
# Render emoticons.
|
||||
if post["emoticons"]:
|
||||
post["body"] = Emoticons.render(post["body"])
|
||||
post["rendered_body"] = Emoticons.render(post["rendered_body"])
|
||||
|
||||
# Get the author's information.
|
||||
post["profile"] = User.get_user(uid=post["author"])
|
||||
|
@ -85,6 +91,7 @@ def update():
|
|||
author=g.info["session"]["uid"],
|
||||
subject="",
|
||||
body="",
|
||||
format="markdown",
|
||||
avatar="",
|
||||
categories="",
|
||||
privacy=BLOG_DEFAULT_PRIVACY,
|
||||
|
@ -110,7 +117,7 @@ def update():
|
|||
g.info["post"] = post
|
||||
|
||||
# Copy fields.
|
||||
for field in ["author", "fid", "subject", "body", "avatar",
|
||||
for field in ["author", "fid", "subject", "format", "body", "avatar",
|
||||
"categories", "privacy", "emoticons", "comments"]:
|
||||
g.info[field] = post[field]
|
||||
|
||||
|
@ -141,6 +148,17 @@ def update():
|
|||
# What action are they doing?
|
||||
if action == "preview":
|
||||
g.info["preview"] = True
|
||||
|
||||
# Render markdown?
|
||||
if g.info["format"] == "markdown":
|
||||
g.info["rendered_body"] = render_markdown(g.info["body"])
|
||||
else:
|
||||
g.info["rendered_body"] = g.info["body"]
|
||||
|
||||
# Render emoticons.
|
||||
if g.info["emoticons"]:
|
||||
g.info["rendered_body"] = Emoticons.render(g.info["rendered_body"])
|
||||
|
||||
elif action == "publish":
|
||||
# Publishing! Validate inputs first.
|
||||
invalid = False
|
||||
|
@ -188,6 +206,7 @@ def update():
|
|||
ip = request.remote_addr,
|
||||
emoticons = g.info["emoticons"],
|
||||
comments = g.info["comments"],
|
||||
format = g.info["format"],
|
||||
body = g.info["body"],
|
||||
)
|
||||
|
||||
|
@ -365,9 +384,15 @@ def partial_index():
|
|||
|
||||
post["post_id"] = post_id
|
||||
|
||||
# Render the body.
|
||||
if post["format"] == "markdown":
|
||||
post["rendered_body"] = render_markdown(post["body"])
|
||||
else:
|
||||
post["rendered_body"] = post["body"]
|
||||
|
||||
# Render emoticons.
|
||||
if post["emoticons"]:
|
||||
post["body"] = Emoticons.render(post["body"])
|
||||
post["rendered_body"] = Emoticons.render(post["rendered_body"])
|
||||
|
||||
# Get the author's information.
|
||||
post["profile"] = User.get_user(uid=post["author"])
|
||||
|
|
|
@ -2,12 +2,14 @@
|
|||
|
||||
from flask import g, session, request, render_template, flash, redirect, url_for
|
||||
from functools import wraps
|
||||
import codecs
|
||||
import uuid
|
||||
import datetime
|
||||
import time
|
||||
import re
|
||||
import importlib
|
||||
import smtplib
|
||||
import markdown
|
||||
|
||||
from rophako.log import logger
|
||||
from config import *
|
||||
|
@ -54,6 +56,54 @@ def template(name, **kwargs):
|
|||
return html
|
||||
|
||||
|
||||
def markdown_template(path):
|
||||
"""Render a Markdown page to the browser."""
|
||||
|
||||
# The path is the absolute path to the Markdown file, so open it directly.
|
||||
fh = codecs.open(path, "r", "utf-8")
|
||||
body = fh.read()
|
||||
fh.close()
|
||||
|
||||
# Extract a title from the first line.
|
||||
first = body.split("\n")[0]
|
||||
if first.startswith("#"):
|
||||
first = first[1:].strip()
|
||||
|
||||
rendered = render_markdown(body)
|
||||
return template("markdown.inc.html",
|
||||
title=first,
|
||||
markdown=rendered,
|
||||
)
|
||||
|
||||
|
||||
def render_markdown(body, html_escape=True):
|
||||
"""Render a block of Markdown text.
|
||||
|
||||
This will default to escaping literal HTML characters. Set
|
||||
`html_escape=False` to trust HTML."""
|
||||
|
||||
args = dict(
|
||||
lazy_ol=False, # If a numbered list starts at e.g. 4, show the <ol> there
|
||||
extensions=[
|
||||
"fenced_code", # GitHub style code blocks
|
||||
"tables", # http://michelf.ca/projects/php-markdown/extra/#table
|
||||
"smart_strong", # Handles double__underscore better.
|
||||
"codehilite", # Code highlighting with Pygment!
|
||||
"nl2br", # Line breaks inside a paragraph become <br>
|
||||
"sane_lists", # Make lists less surprising
|
||||
],
|
||||
extension_configs={
|
||||
"codehilite": {
|
||||
"linenums": False,
|
||||
}
|
||||
}
|
||||
)
|
||||
if html_escape:
|
||||
args["safe_mode"] = "escape"
|
||||
|
||||
return markdown.markdown(body, **args)
|
||||
|
||||
|
||||
def send_email(to, subject, message, sender=None):
|
||||
"""Send an e-mail out."""
|
||||
if sender is None:
|
||||
|
|
|
@ -26,7 +26,7 @@
|
|||
on <span title="{{ post['time'] }}">{{ post["pretty_time"] }}</span>
|
||||
</div>
|
||||
|
||||
{{ post["body"] | safe }}
|
||||
{{ post["rendered_body"] | safe }}
|
||||
|
||||
<p>
|
||||
<div class="clear">
|
||||
|
|
|
@ -5,7 +5,7 @@
|
|||
{% if preview %}
|
||||
<h1>Preview: {{ subject }}</h1>
|
||||
|
||||
{{ body|safe }}
|
||||
{{ rendered_body|safe }}
|
||||
|
||||
<hr>
|
||||
{% endif %}
|
||||
|
@ -26,6 +26,12 @@
|
|||
<input type="text" size="80" name="fid" value="{{ fid }}"><p>
|
||||
|
||||
<strong>Body:</strong><br>
|
||||
<label>
|
||||
<input type="radio" name="format" value="markdown"{% if format == "markdown" %} checked{% endif %}> Markdown
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" name="format" value="html"{% if format == "html" %} checked{% endif %}> HTML
|
||||
</label><br>
|
||||
<textarea cols="80" rows="12" name="body">{{ body }}</textarea><br>
|
||||
<a href="{{ url_for('emoticons.index') }}" target="_blank">Emoticon reference</a> (opens in new window)<p>
|
||||
|
||||
|
|
|
@ -31,6 +31,7 @@ There {% if comments|length == 1 %}is{% else %}are{% endif %}
|
|||
</div><p>
|
||||
{% endfor %}
|
||||
|
||||
<a name="comments"></a>
|
||||
<h2>Add a Comment</h2>
|
||||
|
||||
<form name="comment" action="{{ url_for('comment.preview') }}" method="POST">
|
||||
|
@ -56,7 +57,8 @@ There {% if comments|length == 1 %}is{% else %}are{% endif %}
|
|||
Your Email:
|
||||
</td>
|
||||
<td align="left" valign="middle">
|
||||
<input type="text" size="40" name="contact"> <small>(optional)</small>
|
||||
<input type="text" size="40" name="contact">
|
||||
<small>(optional)</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
@ -65,8 +67,9 @@ There {% if comments|length == 1 %}is{% else %}are{% endif %}
|
|||
</td>
|
||||
<td align="left" valign="top">
|
||||
<textarea cols="40" rows="8" name="message" style="width: 100%"></textarea><br>
|
||||
<small>You can use <a href="{{ url_for('emoticons.index') }}" target="_blank">emoticons</a>
|
||||
in your comment. <em>(opens in a new window)</em></small>
|
||||
<small>Comments can be formatted with <a href="https://daringfireball.net/projects/markdown/syntax" target="_blank">Markdown</a>,
|
||||
and you can use<br><a href="{{ url_for('emoticons.index') }}" target="_blank">emoticons</a>
|
||||
in your comment.</small>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
|
|
71
rophako/www/css/codehilite.css
Normal file
71
rophako/www/css/codehilite.css
Normal file
|
@ -0,0 +1,71 @@
|
|||
/* Syntax highlighting classes for markdown codehilite plugin, which uses
|
||||
Pygments. This file was generated by doing this in the Python shell:
|
||||
|
||||
>>> from pygments.formatters import HtmlFormatter
|
||||
>>> fh = open("codehilite.css", "w")
|
||||
>>> fh.write(HtmlFormatter().get_style_defs(".codehilite"))
|
||||
>>> fh.close()
|
||||
*/
|
||||
|
||||
.codehilite .hll { background-color: #ffffcc }
|
||||
.codehilite { background: #f8f8f8; }
|
||||
.codehilite .c { color: #408080; 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 .cm { color: #408080; font-style: italic } /* Comment.Multiline */
|
||||
.codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
|
||||
.codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
|
||||
.codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
|
||||
.codehilite .gd { color: #A00000 } /* Generic.Deleted */
|
||||
.codehilite .ge { font-style: italic } /* Generic.Emph */
|
||||
.codehilite .gr { color: #FF0000 } /* Generic.Error */
|
||||
.codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
|
||||
.codehilite .gi { color: #00A000 } /* Generic.Inserted */
|
||||
.codehilite .go { color: #888888 } /* 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: #7D9029 } /* 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: #999999; font-weight: bold } /* Name.Entity */
|
||||
.codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
|
||||
.codehilite .nf { color: #0000FF } /* Name.Function */
|
||||
.codehilite .nl { color: #A0A000 } /* 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 .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 .sb { color: #BA2121 } /* Literal.String.Backtick */
|
||||
.codehilite .sc { color: #BA2121 } /* Literal.String.Char */
|
||||
.codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
|
||||
.codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
|
||||
.codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
|
||||
.codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
|
||||
.codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
|
||||
.codehilite .sx { color: #008000 } /* Literal.String.Other */
|
||||
.codehilite .sr { color: #BB6688 } /* 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 .vc { color: #19177C } /* Name.Variable.Class */
|
||||
.codehilite .vg { color: #19177C } /* Name.Variable.Global */
|
||||
.codehilite .vi { color: #19177C } /* Name.Variable.Instance */
|
||||
.codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
|
|
@ -3,6 +3,7 @@
|
|||
<head>
|
||||
<title>{% block title %}{% endblock %}</title>
|
||||
|
||||
<link rel="stylesheet" type="text/css" href="/css/codehilite.css">
|
||||
<link rel="stylesheet" type="text/css" href="/smoke/style.css">
|
||||
</head>
|
||||
<body>
|
||||
|
|
7
rophako/www/markdown.inc.html
Normal file
7
rophako/www/markdown.inc.html
Normal file
|
@ -0,0 +1,7 @@
|
|||
{% extends "layout.html" %}
|
||||
{% block title %}{{ title }}{% endblock %}
|
||||
{% block content %}
|
||||
|
||||
{{ markdown|safe }}
|
||||
|
||||
{% endblock %}
|
|
@ -18,6 +18,10 @@ body {
|
|||
padding: 0;
|
||||
}
|
||||
|
||||
small {
|
||||
font-size: 9pt;
|
||||
}
|
||||
|
||||
a:link, a:visited {
|
||||
color: #FF4400;
|
||||
text-decoration: underline;
|
||||
|
@ -62,6 +66,17 @@ fieldset legend {
|
|||
font-weight: bold;
|
||||
}
|
||||
|
||||
/* Markdown Code-Hilite class overrides. */
|
||||
.codehilite {
|
||||
border: 1px dashed #000000;
|
||||
padding: 0px 20px;
|
||||
background-color: transparent;
|
||||
}
|
||||
pre, code {
|
||||
font-family: "DejaVu Sans Mono","Ubuntu Mono","Lucida Console","Courier New","Courier",monospace;
|
||||
font-size: 10pt;
|
||||
}
|
||||
|
||||
/*********************
|
||||
* Classes used by core Rophako pages. You'll want to
|
||||
* implement these in your custom layout.
|
||||
|
|
Loading…
Reference in New Issue
Block a user