summaryrefslogtreecommitdiff
path: root/pastepy
diff options
context:
space:
mode:
authorJon Bergli Heier <snakebite@jvnv.net>2019-03-15 18:45:47 +0100
committerJon Bergli Heier <snakebite@jvnv.net>2019-03-15 18:45:47 +0100
commitc57494ae405ffbdd8ec45f74ef04ea85895f1bf7 (patch)
treea0b888069205b7d8260c1862d300032a56b6fd30 /pastepy
parentc5ebdf4428d9d9aab04cf6085a231ed6ea5bf0ab (diff)
Port application to flask
Diffstat (limited to 'pastepy')
-rw-r--r--pastepy/__init__.py8
-rw-r--r--pastepy/db.py68
-rw-r--r--pastepy/pastepy.py169
-rw-r--r--pastepy/static/edit.js83
-rw-r--r--pastepy/static/paste.css23
-rw-r--r--pastepy/static/view.js26
-rw-r--r--pastepy/templates/base.html20
-rw-r--r--pastepy/templates/full.html18
-rw-r--r--pastepy/templates/paste.html34
-rw-r--r--pastepy/templates/view.html28
10 files changed, 477 insertions, 0 deletions
diff --git a/pastepy/__init__.py b/pastepy/__init__.py
new file mode 100644
index 0000000..0cc4db6
--- /dev/null
+++ b/pastepy/__init__.py
@@ -0,0 +1,8 @@
+from flask import Flask
+
+app = Flask(__name__)
+app.config.from_pyfile('pastepy.cfg')
+
+with app.app_context():
+ from .pastepy import app as pastepy
+ app.register_blueprint(pastepy)
diff --git a/pastepy/db.py b/pastepy/db.py
new file mode 100644
index 0000000..0aa04a1
--- /dev/null
+++ b/pastepy/db.py
@@ -0,0 +1,68 @@
+from contextlib import contextmanager
+
+from flask import current_app
+from sqlalchemy import create_engine, Column, Integer, String, DateTime, Text, Index, ForeignKey
+from sqlalchemy.ext.declarative import declarative_base
+from sqlalchemy.orm import sessionmaker, relation, backref
+from sqlalchemy.orm.exc import NoResultFound
+
+engine = create_engine(current_app.config['DB_PATH'])
+
+Base = declarative_base(bind = engine)
+
+class Paste(Base):
+ __tablename__ = 'paste'
+
+ id = Column(Integer, primary_key = True)
+ hash = Column(String, unique = True, index = True)
+ nick = Column(String)
+ date = Column(DateTime, nullable = False)
+ syntax = Column(String)
+ title = Column(String)
+ text = Column(Text, nullable = False)
+ ip = Column(String)
+
+ def __init__(self, hash, nick, date, syntax, title, text, ip=None):
+ self.nick = nick
+ self.hash = hash
+ self.date = date
+ self.syntax = syntax
+ self.title = title
+ self.text = text
+ self.ip = ip
+
+ def __repr__(self):
+ return '<Paste(%d, "%s", "%s", "%s", "%s")>' % (self.id, self.hash, self.nick, self.date.ctime(), self.title)
+
+class Cache(Base):
+ __tablename__ = 'cache'
+
+ id = Column(Integer, primary_key = True)
+ paste_hash = Column(String, ForeignKey('paste.hash'), index = True)
+ paste = relation(Paste, primaryjoin = paste_hash == Paste.hash, lazy = False)
+ syntax_name = Column(String)
+ text = Column(Text, nullable = False)
+
+ def __init__(self, hash, syntax_name, text):
+ self.paste_hash = hash
+ self.syntax_name = syntax_name
+ self.text = text
+
+ def __repr__(self):
+ return '<Cache(%d, "%s")>' % (self.id, self.paste_hash)
+
+Base.metadata.create_all()
+Session = sessionmaker(bind = engine, autoflush = True, autocommit = False)
+
+@contextmanager
+def session_scope():
+ session = Session()
+ try:
+ session.expire_on_commit = False
+ yield session
+ session.commit()
+ except:
+ session.rollback()
+ raise
+ finally:
+ session.close()
diff --git a/pastepy/pastepy.py b/pastepy/pastepy.py
new file mode 100644
index 0000000..750d8fe
--- /dev/null
+++ b/pastepy/pastepy.py
@@ -0,0 +1,169 @@
+import datetime
+import random
+
+from flask import Blueprint, current_app, render_template, request, session, redirect, url_for
+try:
+ import markdown
+ has_markdown = True
+except ImportError:
+ has_markdown = False
+import pygments
+from pygments import highlight
+from pygments.lexers import get_all_lexers, get_lexer_by_name
+from pygments.formatters import HtmlFormatter
+
+from . import db
+
+app = Blueprint(__name__, 'pastepy')
+
+base62_alphabet = '0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ'
+
+class PasteError(Exception): pass
+class UnknownSyntaxError(PasteError): pass
+
+class CustomHtmlFormatter(HtmlFormatter):
+ def wrap(self, source, outfile):
+ yield 0, '<pre>'
+ line = 1
+ for i, t in source:
+ yield i, '<span id="codeline-{}">{}</span>'.format(i, t)
+ line += 1
+ yield 0, '</pre>'
+
+def init_lexers():
+ all_lexers = dict([(x[0], x[1][0]) for x in get_all_lexers()])
+ lexers = []
+ removed = []
+ for cat, ls in current_app.config['CATEGORIES']:
+ for l in ls:
+ removed.append(l)
+ lexers.append((cat, [(x, all_lexers[x]) for x in ls if x in all_lexers]))
+ for l in removed:
+ try:
+ del all_lexers[l]
+ except KeyError:
+ pass
+ rendered = []
+ if has_markdown:
+ rendered.append(('Rendered markdown', 'md-render'))
+ if len(rendered):
+ lexers.append(('Rendered', rendered))
+ lexers.append(('Others' if current_app.config['CATEGORIES'] else 'Syntax', sorted(all_lexers.items(), key = lambda l: l[0].lower())))
+
+ current_app.config['LEXERS'] = lexers
+ current_app.config['FORMATTER'] = CustomHtmlFormatter(linenos='table', lineanchors='line', anchorlinenos=True)
+
+init_lexers()
+
+@app.route('/')
+@app.route('/paste')
+def paste():
+ context = {
+ 'title': current_app.config['PASTEBIN_NAME'],
+ 'header': current_app.config['PASTEBIN_NAME'],
+ }
+ return render_template('paste.html', **context)
+
+def get_formatted(syntax, text):
+ if syntax == 'md-render' and has_markdown:
+ text = markdown.markdown(text, extensions=current_app.config['MARKDOWN_EXTENSIONS'])
+ lexername = 'Rendered markdown'
+ else:
+ try:
+ lex = get_lexer_by_name(syntax or 'text')
+ except pygments.util.ClassNotFound:
+ raise UnknownSyntaxError(syntax)
+ lexername = lex.name
+ text = highlight(text, lex, current_app.config['FORMATTER'])
+ return (lexername, text)
+
+def add_paste():
+ nick = request.form.get('nick')
+ syntax = request.form.get('syntax')
+ title = request.form.get('title')
+ text = request.form.get('text').replace('\r', '')
+
+ with db.session_scope() as db_session:
+ paste_hash = ''.join(random.choice(base62_alphabet) for x in range(5))
+ paste = db.Paste(paste_hash, nick, datetime.datetime.utcnow(), syntax, title, text, request.remote_addr)
+ db_session.add(paste)
+
+ if 'remember_me' in request.form:
+ session['nick'] = nick
+ else:
+ session.pop('nick', None)
+ if 'remember_syntax' in request.form:
+ session['syntax'] = syntax
+ else:
+ session.pop('syntax', None)
+ return redirect(url_for('.view_paste', paste_hash=paste_hash))
+
+def render_preview():
+ try:
+ lexername, text = get_formatted(request.form['syntax'], request.form['text'])
+ except UnknownSyntaxError:
+ abort(500, 'Could not find lexer "{}".'.format(request.form['syntax']))
+
+ nick = request.form['nick'] or 'Anonymous'
+ title = request.form['title'] or 'Untitled'
+ return render_template('view.html',
+ title='Preview',
+ hash=None,
+ nick=nick,
+ date='{} UTC'.format(datetime.datetime.utcnow().ctime()),
+ syntax=lexername,
+ pastetitle=title,
+ text=text,
+ rendered=(lexername or '').startswith('Rendered '),
+ )
+
+@app.route('/paste', methods=['POST'])
+def post_paste():
+ if request.form['type'] == 'Preview':
+ return render_preview()
+ if request.form['type'] == 'Paste':
+ return add_paste()
+ abort(404)
+
+@app.route('/view/<paste_hash>')
+@app.route('/full/<paste_hash>', endpoint='view_full')
+def view_paste(paste_hash):
+ with db.session_scope() as db_session:
+ try:
+ cache = db_session.query(db.Cache).filter_by(paste_hash=paste_hash).one()
+ paste = cache.paste
+ except db.NoResultFound:
+ try:
+ paste = db_session.query(db.Paste).filter_by(hash=paste_hash).one()
+ except db.NoResultFound:
+ abort(404)
+ try:
+ lexername, text = get_formatted(paste.syntax, paste.text)
+ except UnknownSyntaxError:
+ abort(500, 'Could not find the lexer "{}"'.format(paste.syntax))
+ cache = db.Cache(paste_hash, lexername, text)
+ db_session.add(cache)
+ template = 'full.html' if request.endpoint.endswith('.view_full') else 'view.html'
+ return render_template(template,
+ title='View paste \u2013 {}'.format(current_app.config['PASTEBIN_NAME'], paste.title or 'Untitled'),
+ hash=paste_hash,
+ date='{} UTC'.format(paste.date.ctime()),
+ nick=paste.nick or 'Anonymous',
+ syntax=cache.syntax_name,
+ pastetitle=paste.title or 'Untitled',
+ text=cache.text,
+ rendered=(cache.syntax_name or '').startswith('Rendered '),
+ )
+
+@app.route('/highlight_stylesheet')
+def highlight_stylesheet():
+ return current_app.config['FORMATTER'].get_style_defs(), 200, {'Content-Type': 'text/css'}
+
+@app.route('/raw/<paste_hash>')
+def raw(paste_hash):
+ with db.session_scope() as db_session:
+ try:
+ paste = db_session.query(db.Paste).filter_by(hash=paste_hash).one()
+ except db.NoResultFound:
+ abort(404)
+ return paste.text, 200, {'Content-Type': 'text/plain'}
diff --git a/pastepy/static/edit.js b/pastepy/static/edit.js
new file mode 100644
index 0000000..8e754d8
--- /dev/null
+++ b/pastepy/static/edit.js
@@ -0,0 +1,83 @@
+function tab_add(t, ti, ss, se) {
+ t.value = t.value.substr(0, ti) + "\t" + t.value.substr(ti);
+ t.selectionStart = ss + 1;
+ t.selectionEnd = se + 1;
+}
+
+function tab_remove(t, ti, ss, se) {
+ t.value = t.value.substr(0, ti-1) + t.value.substr(ti);
+ t.selectionStart = ss - 1;
+ t.selectionEnd = se - 1;
+}
+
+function textarea_handle_tab(t, e) {
+ if(navigator.userAgent.match("Gecko")) {
+ keyCode = e.which;
+ } else {
+ keyCode = e.keyCode;
+ }
+ if(keyCode != 9)
+ return;
+ e.preventDefault();
+ var ss = t.selectionStart;
+ var se = t.selectionEnd;
+ if(ss == se) {
+ if(e.shiftKey) {
+ if(t.value[ss-1] == "\t") {
+ tab_remove(t, ss, ss, se);
+ }
+ } else {
+ tab_add(t, ss, ss, se);
+ }
+ } else {
+ var s;
+ if(ss < se)
+ s = t.value.substr(ss, se - ss);
+ else
+ s = t.value.substr(se, ss - se);
+ if(s.indexOf("\n") > -1) {
+ var nl = s.lastIndexOf("\n");
+ if(e.shiftKey) {
+ while(nl > -1) {
+ if(s[nl+1] == "\t") {
+ tab_remove(t, ss + nl + 2, ss, se);
+ se--;
+ }
+ nl = s.lastIndexOf("\n", nl - 2);
+ }
+ if(t.value[(ss < se ? ss : se)-1] == "\n" && e.shiftKey)
+ tab_remove(t, ss+1, ss, se);
+ if((ss == 0 || se == 0) && t.value[0] == "\t")
+ tab_remove(t, 1, ss, se);
+ } else {
+ while(nl > -1) {
+ tab_add(t, ss + nl + 1, ss, se);
+ se++;
+ if(nl == 0)
+ break;
+ nl = s.lastIndexOf("\n", nl - 2);
+ }
+ if(ss == 0 || se == 0 || t.value[(ss < se ? ss : se)-1] == "\n")
+ tab_add(t, ss, ss-1, se);
+ }
+
+ } else {
+ var nl = t.value.lastIndexOf("\n", (ss < se ? se : ss) - 1) + 1;
+ if(e.shiftKey) {
+ if(t.value[nl] == "\t") {
+ tab_remove(t, nl + 1, ss, se);
+ }
+ } else {
+ tab_add(t, nl, ss, se);
+ }
+ }
+ }
+ setTimeout("document.getElementById('" + t.id + "').focus();", 0);
+}
+
+window.onload = function() {
+ text = document.getElementById("text");
+ text.onkeydown = function(event) {
+ return textarea_handle_tab(text, event);
+ }
+}
diff --git a/pastepy/static/paste.css b/pastepy/static/paste.css
new file mode 100644
index 0000000..5e59d4d
--- /dev/null
+++ b/pastepy/static/paste.css
@@ -0,0 +1,23 @@
+html, body { margin: 0; padding: 0; font-family: sans-serif; background-color: #fff; color: #000; }
+div#page { background: #eee; margin: 1em; }
+div#page-header, div#page-content { padding: 1em; }
+div#page-header h1 a { color: inherit; }
+div#page-header h1 a:hover { text-decoration: none; }
+a { color: #00f; text-decoration: none; }
+a:hover { text-decoration: underline; }
+form#pasteform ul { list-style-type: none; }
+textarea { font-family: monospace; }
+div#info { font-size: small; color: #bbb; }
+span#nick, span#date, span#syntax-type { color: #888; }
+div#alt-links { font-size: small; }
+div#alt-links a { color: #44f; }
+div.paste-rendered { padding: 1em; }
+div#paste div.paste-rendered { background-color: #fff; margin-top: 1em; }
+.highlighttable { width: 100%; padding-top: 1em; }
+.highlighttable td.code { width: 100%; background-color: #fff; }
+.highlighttable td { vertical-align: top; }
+.highlighttable pre { margin: 0; padding: 0; }
+.highlighttable .linenos { text-align: right; padding-left: .5em; padding-right: .5em; }
+.highlighttable .linenos a { color: #888; }
+.highlighttable .selected { display: block; background-color: #cfc; }
+.highlighttable .code { tab-size: 4; }
diff --git a/pastepy/static/view.js b/pastepy/static/view.js
new file mode 100644
index 0000000..fd6db03
--- /dev/null
+++ b/pastepy/static/view.js
@@ -0,0 +1,26 @@
+function set_highlight(line) {
+ var line = 'codeline-' + line
+ var codeline = document.getElementById(line);
+ if (codeline) {
+ codeline.className = 'selected';
+ }
+}
+function cleanup() {
+ var elements = document.getElementsByClassName('selected');
+ for (var i = 0; i < elements.length; i++) {
+ elements[i].className = '';
+ }
+}
+window.onload = function(event) {
+ var line = document.location.hash.substr(6);
+ if (line) {
+ set_highlight(line);
+ }
+ var as = document.getElementsByClassName('linenodiv')[0].getElementsByTagName('a');
+ for (var i = 0; i < as.length; i++) {
+ as[i].onclick = function(event) {
+ cleanup();
+ set_highlight(event.target.text.trim());
+ }
+ }
+}
diff --git a/pastepy/templates/base.html b/pastepy/templates/base.html
new file mode 100644
index 0000000..e208b6b
--- /dev/null
+++ b/pastepy/templates/base.html
@@ -0,0 +1,20 @@
+<!DOCTYPE html>
+<html lang="en">
+ <head>
+ <title>{{ config.PASTEBIN_NAME }}{% if title %} &ndash; {{ title }}{% endif %}</title>
+ <link rel="StyleSheet" href="{{ url_for('static', filename='paste.css') }}" type="text/css">
+{% block head %}
+{% endblock %}
+ </head>
+ <body>
+ <div id="page">
+ <div id="page-header">
+ <h1><a href="/">{{ title }}</a></h1>
+ </div>
+ <div id="page-content">
+{% block content %}
+{% endblock %}
+ </div>
+ </div>
+ </body>
+</html>
diff --git a/pastepy/templates/full.html b/pastepy/templates/full.html
new file mode 100644
index 0000000..c60126e
--- /dev/null
+++ b/pastepy/templates/full.html
@@ -0,0 +1,18 @@
+<!DOCTYPE html>
+<html lang="en">
+<head>
+ <title>{{ title }}</title>
+ <link rel="StyleSheet" href="{{ url_for('.highlight_stylesheet') }}" type="text/css">
+ <link rel="StyleSheet" href="{{ url_for('static', filename='paste.css') }}" type="text/css">
+ <script type="text/javascript" src="{{ url_for('static', filename='view.js') }}"></script>
+</head>
+<body>
+{% if rendered %}
+<div class="paste-rendered">
+{% endif %}
+{{ text|safe }}
+{% if rendered %}
+</div>
+{% endif %}
+</body>
+</html>
diff --git a/pastepy/templates/paste.html b/pastepy/templates/paste.html
new file mode 100644
index 0000000..9a22e76
--- /dev/null
+++ b/pastepy/templates/paste.html
@@ -0,0 +1,34 @@
+{% extends "base.html" %}
+{% block head %}
+ <script src="{{ url_for('static', filename='edit.js') }}" type="text/javascript"></script>
+{% endblock %}
+{% block content %}
+ <form action="/paste" method="post" id="pasteform">
+ <ul>
+ <li id="syntaxli">
+ <select name="syntax" id="syntax">
+ {% for cat, ls in config.LEXERS %}
+ <optgroup label="{{ cat }}">
+ {% for k, v in ls %}
+ <option value="{{ v }}"{% if v == (session.syntax or config.DEFAULT_SYNTAX) %} selected="selected"{% endif %}>{{ k }}</option>
+ {% endfor %}
+ </optgroup>
+ {% endfor %}
+ </select>
+ <input type="checkbox" name="remember_syntax" id="remember_syntax"{% if session.syntax %} checked{% endif %}>
+ <label for="remember_syntax">Remember syntax</label>
+ </li>
+ <li>
+ <input type="text" name="nick" id="nick" value="{{ session.nick }}">
+ <input type="checkbox" name="remember_me" id="remember_me"{% if session.nick %} checked{% endif %}>
+ <label for="remember_me">Remember me</label>
+ </li>
+ <li><input type="text" name="title" id="title" value="Untitled"></li>
+ <li><textarea rows="15" cols="80" name="text" id="text"></textarea></li>
+ <li>
+ <input type="submit" id="pastesubmit" name="type" value="Paste">
+ <input type="submit" id="pastesubmit2" name="type" value="Preview">
+ </li>
+ </ul>
+ </form>
+{% endblock %}
diff --git a/pastepy/templates/view.html b/pastepy/templates/view.html
new file mode 100644
index 0000000..b0b114c
--- /dev/null
+++ b/pastepy/templates/view.html
@@ -0,0 +1,28 @@
+{% extends "base.html" %}
+{% block head %}
+ <link rel="StyleSheet" href="{{ url_for('.highlight_stylesheet') }}" type="text/css">
+ <script type="text/javascript" src="{{ url_for('static', filename='view.js') }}"></script>
+{% endblock %}
+{% block content %}
+ <p><a href="/">New paste</a></p>
+ <div id="paste">
+ <h3>{{ pastetitle }}</h3>
+ <div id="info">Pasted by <span id="nick">{{ nick }}</span> on <span id="date">{{date }}</span>
+ {% if syntax %} as <span id="syntax-type">{{ syntax }}</span>{% endif %}
+ </div>
+ <div id="alt-links">
+ {% if hash %}
+ <a href="/raw/{{ hash }}">Download as plain text</a>
+ &mdash;
+ <a href="/full/{{ hash }}">View in fullscreen</a>
+ {% endif %}
+ </div>
+ {% if rendered %}
+ <div class="paste-rendered">
+ {% endif %}
+ {{ text|safe }}
+ {% if rendered %}
+ </div>
+ {% endif %}
+ </div>
+{% endblock %}