@@ -222,7 +222,7 @@ def create_app(subdomain=None): | |||
cname = to_canonical(name) | |||
if request.method == 'POST': | |||
edit_cname = to_canonical(request.form['name']) | |||
if edit_cname != cname: | |||
if edit_cname.lower() != cname.lower(): | |||
w.rename_page(cname, edit_cname) | |||
w.write_page(edit_cname, request.form['content'], message=request.form['message'], | |||
username=CurrentUser.get('username')) | |||
@@ -0,0 +1,105 @@ | |||
/*global module:true*/ | |||
/* | |||
* Basic table support with re-entrant parsing, where cell content | |||
* can also specify markdown. | |||
* | |||
* Tables | |||
* ====== | |||
* | |||
* | Col 1 | Col 2 | | |||
* |======== |====================================================| | |||
* |**bold** | ![Valid XHTML] (http://w3.org/Icons/valid-xhtml10) | | |||
* | Plain | Value | | |||
* | |||
*/ | |||
(function(){ | |||
var table = function(converter) { | |||
var tables = {}, style = 'text-align:left;', filter; | |||
tables.th = function(header){ | |||
if (header.trim() === "") { return "";} | |||
var id = header.trim().replace(/ /g, '_').toLowerCase(); | |||
return '<th id="' + id + '" style="'+style+'">' + header + '</th>'; | |||
}; | |||
tables.td = function(cell) { | |||
return '<td style="'+style+'">' + converter.makeHtml(cell) + '</td>'; | |||
}; | |||
tables.ths = function(){ | |||
var out = "", i = 0, hs = [].slice.apply(arguments); | |||
for (i;i<hs.length;i+=1) { | |||
out += tables.th(hs[i]) + '\n'; | |||
} | |||
return out; | |||
}; | |||
tables.tds = function(){ | |||
var out = "", i = 0, ds = [].slice.apply(arguments); | |||
for (i;i<ds.length;i+=1) { | |||
out += tables.td(ds[i]) + '\n'; | |||
} | |||
return out; | |||
}; | |||
tables.thead = function() { | |||
var out, i = 0, hs = [].slice.apply(arguments); | |||
out = "<thead>\n"; | |||
out += "<tr>\n"; | |||
out += tables.ths.apply(this, hs); | |||
out += "</tr>\n"; | |||
out += "</thead>\n"; | |||
return out; | |||
}; | |||
tables.tr = function() { | |||
var out, i = 0, cs = [].slice.apply(arguments); | |||
out = "<tr>\n"; | |||
out += tables.tds.apply(this, cs); | |||
out += "</tr>\n"; | |||
return out; | |||
}; | |||
filter = function(text) { | |||
var i=0, lines = text.split('\n'), tbl = [], line, hs, rows, out = []; | |||
for (i; i<lines.length;i+=1) { | |||
line = lines[i]; | |||
// looks like a table heading | |||
if (line.trim().match(/^[|]{1}.*[|]{1}$/)) { | |||
line = line.trim(); | |||
tbl.push('<table>'); | |||
hs = line.substring(1, line.length -1).split('|'); | |||
tbl.push(tables.thead.apply(this, hs)); | |||
line = lines[++i]; | |||
if (!line.trim().match(/^[|]{1}[-=| ]+[|]{1}$/)) { | |||
// not a table rolling back | |||
line = lines[--i]; | |||
} | |||
else { | |||
line = lines[++i]; | |||
tbl.push('<tbody>'); | |||
while (line.trim().match(/^[|]{1}.*[|]{1}$/)) { | |||
line = line.trim(); | |||
tbl.push(tables.tr.apply(this, line.substring(1, line.length -1).split('|'))); | |||
line = lines[++i]; | |||
} | |||
tbl.push('</tbody>'); | |||
tbl.push('</table>'); | |||
// we are done with this table and we move along | |||
out.push(tbl.join('\n')); | |||
continue; | |||
} | |||
} | |||
out.push(line); | |||
} | |||
return out.join('\n'); | |||
}; | |||
return [ | |||
{ | |||
type: 'lang', | |||
filter: filter | |||
} | |||
]; | |||
}; | |||
// Client-side export | |||
if (typeof window !== 'undefined' && window.Showdown && window.Showdown.extensions) { window.Showdown.extensions.table = table; } | |||
// Server-side export | |||
if (typeof module !== 'undefined') { | |||
module.exports = table; | |||
} | |||
}()); |
@@ -3,7 +3,15 @@ | |||
* Copyright (c) 2010 Caolan McMahon | |||
*/ | |||
function escapeHtml(s) { | |||
s = ('' + s); /* Coerce to string */ | |||
s = s.replace(/&/g, '&'); | |||
s = s.replace(/</g, '<'); | |||
s = s.replace(/>/g, '>'); | |||
s = s.replace(/"/g, '"'); | |||
s = s.replace(/'/g, '''); | |||
return s; | |||
} | |||
/** | |||
* Main function for converting markdown to HTML. | |||
@@ -87,6 +95,34 @@ WMD.preprocessors = { | |||
} | |||
doc.markdown = lines.join('\n'); | |||
return doc; | |||
}, | |||
fencedCodeBlocksHighlightJS: function (doc) { | |||
var re1 = /```([A-Za-z]+)\s*([\s\S]+?)```/; // with syntax highlighting | |||
var re2 = /```\s*([\s\S]+?)```/; // without syntax highlighting | |||
var block; | |||
while (block = re1.exec(doc.markdown) || re2.exec(doc.markdown)) { | |||
var pre; | |||
if (block.length === 3) { | |||
// we have a code format | |||
pre = '<pre style="padding:0;"><code class="' + escapeHtml(block[1]) + '">'; | |||
if (block[1] in hljs.LANGUAGES) { | |||
pre += hljs.highlight(block[1], block[2]).value; | |||
} | |||
else { | |||
pre += escapeHtml(block[2]); | |||
} | |||
pre += '</code></pre>'; | |||
} | |||
else { | |||
// no syntax highlighting | |||
pre = '<pre style="padding:0;"><code class="no-highlight">' + | |||
escapeHtml(block[1]) + '</code></pre>'; | |||
} | |||
doc.markdown = doc.markdown.substr(0, block.index) + | |||
pre + doc.markdown.substr(block.index + block[0].length); | |||
} | |||
return doc; | |||
} | |||
}; | |||
@@ -104,9 +140,11 @@ WMD.readOptions = function (options) { | |||
var obj = { | |||
preprocessors: [ | |||
WMD.preprocessors.metadata, | |||
WMD.preprocessors.underscores | |||
WMD.preprocessors.underscores, | |||
WMD.preprocessors.fencedCodeBlocksHighlightJS | |||
], | |||
postprocessors: [] | |||
postprocessors: [ | |||
] | |||
}; | |||
for (var k in options) { | |||
obj[k] = options[k]; | |||
@@ -10,6 +10,7 @@ | |||
<link href="/static/css/bootstrap/spacelab.css" rel="stylesheet"> | |||
<link href="/static/css/font-awesome.min.css" rel="stylesheet"> | |||
<link href="/static/js/highlight/styles/github.css" rel="stylesheet"> | |||
<link href="/static/css/style.css" rel="stylesheet"> | |||
<!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries --> | |||
@@ -106,6 +107,7 @@ | |||
<script src="/static/js/showdown/showdown.js"></script> | |||
<script src="/static/js/html-sanitizer-minified.js"></script> | |||
<script src="/static/js/showdown/wmd.js"></script> | |||
<script src="/static/js/highlight/highlight.pack.js"></script> | |||
<script> | |||
/* | |||
marked.setOptions({ | |||
@@ -118,6 +120,11 @@ | |||
langPrefix: 'lang-' | |||
}); | |||
*/ | |||
hljs.initHighlightingOnLoad(); | |||
// Markdown Renderer | |||
MDR = { | |||
doc: null, | |||
callback: WMD, | |||
@@ -65,22 +65,4 @@ | |||
<div id="preview"></div> | |||
</div> | |||
<!-- | |||
<form role="form" method="post"> | |||
<div class="form-group"> | |||
<label for="name"></label> | |||
<input type="text" class="form-control" id="page" name="name" placeholder="Name" value="{{- name -}}" /> | |||
</div> | |||
<div class="form-group"> | |||
<label for="content"></label> | |||
<div id="epiceditor"></div> | |||
<textarea name="content" id="content" class="form-control" placeholder="Content" style="display:none;">{{- content -}}</textarea> | |||
</div> | |||
<input type="submit" class="btn btn-primary" value="Save" /> | |||
</form> | |||
--> | |||
{% endblock %} |
@@ -9,7 +9,7 @@ | |||
{% block body %} | |||
<div id="page-content" style="display:none"> | |||
{{ page.data|safe }} | |||
{{ page.data }} | |||
</div> | |||
{% endblock %} | |||
@@ -3,6 +3,37 @@ import os | |||
import hashlib | |||
def escape_repl(m): | |||
print "group 0" | |||
print m.group(0) | |||
print "group 1" | |||
print m.group(1) | |||
if m.group(1): | |||
return "```" + escape_html(m.group(1)) + "```" | |||
def unescape_repl(m): | |||
if m.group(1): | |||
return "```" + unescape_html(m.group(1)) + "```" | |||
def escape_html(s): | |||
s = s.replace("&", '&') | |||
s = s.replace("<", '<') | |||
s = s.replace(">", '>') | |||
s = s.replace('"', '"') | |||
s = s.replace("'", ''') | |||
return s | |||
def unescape_html(s): | |||
s = s.replace('&', "&") | |||
s = s.replace('<', "<") | |||
s = s.replace('>', ">") | |||
s = s.replace('"', '"') | |||
s = s.replace(''', "'") | |||
return s | |||
def mkdir_safe(path): | |||
if path and not(os.path.exists(path)): | |||
os.makedirs(path) | |||
@@ -1,12 +1,13 @@ | |||
import os | |||
import re | |||
from lxml.html.clean import clean_html | |||
import lxml.html | |||
from lxml.html import clean | |||
import ghdiff | |||
from gittle import Gittle | |||
from dulwich.repo import NotGitRepository | |||
from util import to_canonical | |||
from util import to_canonical, escape_repl, unescape_repl | |||
from models import Site | |||
@@ -68,11 +69,23 @@ class Wiki(): | |||
return True if s.get_by_name(name) else False | |||
def write_page(self, name, content, message=None, create=False, username=None, email=None): | |||
# adding the div wrapper apparently fixes anomalies with the lxml parser with certain markdown | |||
content = clean_html('<div>' + content + '</div>') | |||
# prevents p tag from being added, we remove this later | |||
content = '<div>' + content + '</div>' | |||
content = re.sub(r"```(.*?)```", escape_repl, content, flags=re.DOTALL) | |||
tree = lxml.html.fromstring(content) | |||
cleaner = clean.Cleaner(remove_unknown_tags=False) | |||
tree = cleaner.clean_html(tree) | |||
content = lxml.html.tostring(tree, encoding='utf-8', method='html') | |||
# post processing to fix errors | |||
content = content[5:-6] | |||
content = re.sub(r"(\n>)", "\n>", content) | |||
content = re.sub(r"(^>)", ">", content) | |||
content = re.sub(r"```(.*?)```", unescape_repl, content, flags=re.DOTALL) | |||
filename = self.cname_to_filename(to_canonical(name)) | |||
f = open(self.path + "/" + filename, 'w') | |||
@@ -6,7 +6,7 @@ python-pkgs: | |||
- build-essential | |||
{% for pkg in ['ghdiff', 'tornado', 'pyzmq', 'itsdangerous', 'boto', 'redis', 'simplejson', 'sockjs-tornado', 'flask', 'flask-bcrypt', 'flask-login', 'flask-assets', 'gittle', 'gevent', 'lxml', 'markdown2', 'recaptcha-client', 'RethinkORM' ] %} | |||
{% for pkg in ['BeautifulSoup', 'html5lib', 'ghdiff', 'tornado', 'pyzmq', 'itsdangerous', 'boto', 'redis', 'simplejson', 'sockjs-tornado', 'flask', 'flask-bcrypt', 'flask-login', 'flask-assets', 'gittle', 'gevent', 'lxml', 'markdown2', 'recaptcha-client', 'RethinkORM' ] %} | |||
{{ pkg }}-pip: | |||
pip: | |||
- name: {{ pkg }} | |||