Add Markdown support to blogs, comments, pages

This commit is contained in:
Noah 2014-04-18 21:55:37 -07:00
parent b58563b0d1
commit 978928c97e
13 changed files with 217 additions and 24 deletions

View File

@ -4,3 +4,5 @@ redis
bcrypt
pillow
requests
Markdown
Pygments

View File

@ -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")

View File

@ -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,
))

View File

@ -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'&', '&amp;', message)
message = re.sub(r'<', '&lt;', message)
message = re.sub(r'>', '&gt;', message)
message = re.sub(r'"', '&quot;', message)
message = re.sub(r"'", '&apos;', 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)

View File

@ -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"])

View File

@ -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:

View File

@ -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">

View File

@ -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>

View File

@ -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>

View 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 */

View File

@ -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>

View File

@ -0,0 +1,7 @@
{% extends "layout.html" %}
{% block title %}{{ title }}{% endblock %}
{% block content %}
{{ markdown|safe }}
{% endblock %}

View File

@ -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.