From c57494ae405ffbdd8ec45f74ef04ea85895f1bf7 Mon Sep 17 00:00:00 2001 From: Jon Bergli Heier Date: Fri, 15 Mar 2019 18:45:47 +0100 Subject: Port application to flask --- pastepy/__init__.py | 8 ++ pastepy/db.py | 68 +++++++++++++++++ pastepy/pastepy.py | 169 +++++++++++++++++++++++++++++++++++++++++++ pastepy/static/edit.js | 83 +++++++++++++++++++++ pastepy/static/paste.css | 23 ++++++ pastepy/static/view.js | 26 +++++++ pastepy/templates/base.html | 20 +++++ pastepy/templates/full.html | 18 +++++ pastepy/templates/paste.html | 34 +++++++++ pastepy/templates/view.html | 28 +++++++ 10 files changed, 477 insertions(+) create mode 100644 pastepy/__init__.py create mode 100644 pastepy/db.py create mode 100644 pastepy/pastepy.py create mode 100644 pastepy/static/edit.js create mode 100644 pastepy/static/paste.css create mode 100644 pastepy/static/view.js create mode 100644 pastepy/templates/base.html create mode 100644 pastepy/templates/full.html create mode 100644 pastepy/templates/paste.html create mode 100644 pastepy/templates/view.html (limited to 'pastepy') 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 '' % (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 '' % (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, '
'
+        line = 1
+        for i, t in source:
+            yield i, '{}'.format(i, t)
+            line += 1
+        yield 0, '
' + +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/') +@app.route('/full/', 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/') +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 @@ + + + + {{ config.PASTEBIN_NAME }}{% if title %} – {{ title }}{% endif %} + +{% block head %} +{% endblock %} + + +
+ +
+{% block content %} +{% endblock %} +
+
+ + 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 @@ + + + + {{ title }} + + + + + +{% if rendered %} +
+{% endif %} +{{ text|safe }} +{% if rendered %} +
+{% endif %} + + 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 %} + +{% endblock %} +{% block content %} +
+
    +
  • + + + +
  • +
  • + + + +
  • +
  • +
  • +
  • + + +
  • +
+
+{% 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 %} + + +{% endblock %} +{% block content %} +

New paste

+
+

{{ pastetitle }}

+
Pasted by {{ nick }} on {{date }} + {% if syntax %} as {{ syntax }}{% endif %} +
+ + {% if rendered %} +
+ {% endif %} + {{ text|safe }} + {% if rendered %} +
+ {% endif %} +
+{% endblock %} -- cgit v1.2.3