diff options
author | Jon Bergli Heier <snakebite@jvnv.net> | 2019-03-15 18:45:47 +0100 |
---|---|---|
committer | Jon Bergli Heier <snakebite@jvnv.net> | 2019-03-15 18:45:47 +0100 |
commit | c57494ae405ffbdd8ec45f74ef04ea85895f1bf7 (patch) | |
tree | a0b888069205b7d8260c1862d300032a56b6fd30 /pastepy | |
parent | c5ebdf4428d9d9aab04cf6085a231ed6ea5bf0ab (diff) |
Port application to flask
Diffstat (limited to 'pastepy')
-rw-r--r-- | pastepy/__init__.py | 8 | ||||
-rw-r--r-- | pastepy/db.py | 68 | ||||
-rw-r--r-- | pastepy/pastepy.py | 169 | ||||
-rw-r--r-- | pastepy/static/edit.js | 83 | ||||
-rw-r--r-- | pastepy/static/paste.css | 23 | ||||
-rw-r--r-- | pastepy/static/view.js | 26 | ||||
-rw-r--r-- | pastepy/templates/base.html | 20 | ||||
-rw-r--r-- | pastepy/templates/full.html | 18 | ||||
-rw-r--r-- | pastepy/templates/paste.html | 34 | ||||
-rw-r--r-- | pastepy/templates/view.html | 28 |
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 %} – {{ 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> + — + <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 %} |