from . import data, css from ll.xist import xsc from ll.xist.ns import html from flask import Flask, url_for import peewee as pw import re import difflib class DiffSupport: def __init__(self): self.inline_style = False def _diff(self, line1, line2): diff = list(difflib._mdiff([line1], [line2])) return (self._format_diff(diff[0][0][1]), self._format_diff(diff[0][1][1])) def _diff_lines(self, lines1, lines2): diff = difflib._mdiff(lines1, lines2) rows = [] in_change = False for ((line1, diff1), (line2, diff2), flag) in diff: if flag ^ in_change: if flag: symbol = '↓' else: symbol = '↑' rows.append(html.tr(html.td(' '.join(symbol * 10)), html.td(' '.join(symbol * 10)))) in_change = flag rows.append(html.tr( html.td(*self._format_diff(diff1)), html.td(*self._format_diff(diff2)) )) return html.table(rows, **{'class': 'textdiff'}) def _format_diff(self, line): actionclass = { '+': 'diff_add', '-': 'diff_sub', '^': 'diff_chg'} actionname = { '+': 'strong', '-': 'strong', '^': 'strong'} elems = [] nextpos = line.find('\x00') while nextpos != -1 and nextpos + 1 < len(line): endpos = line.find('\x01', nextpos + 2) if nextpos != 0: # intermediate unchanged text elems += [html.span(line[:nextpos])] text = line[nextpos + 2:endpos] if self.inline_style: elem = html.span(text) elem.xmlname = actionname[line[nextpos + 1]] elems += [elem] else: elems += [html.span(text, ** {'class': actionclass[line[nextpos + 1]]})] line = line[endpos:] nextpos = line.find('\x00') if line != '': # trailing unchanged text elems += [html.span(line)] elems += [html.br()] return elems class Difftable(html.div, DiffSupport): def __init__(self, to_version, from_version=None, inline_style=False): html.div.__init__(self) DiffSupport.__init__(self) self.xmlname = 'div' self.inline_style = inline_style if from_version == None: self.single_version(to_version) else: self.two_versions(to_version, from_version) def single_version(self, version): self.append(html.table( html.tr(html.th('Title'), html.td(html.a(version.title, href=version.url))), html.tr(html.th('Authors'), html.td(version.authors)), html.tr(html.th('Date'), html.td(version.created_date.strftime('%x %X'))), html.tr(html.th('Text', colspan=2)), html.tr(html.td(map(html.p, version.text.split('\n')), colspan=2)) )) def two_versions(self, to_version, from_version): def prepare_text(text): return re.sub('\n\n\n*', '\n\n', text).split('\n') from_text = prepare_text(from_version.text) to_text = prepare_text(to_version.text) from_difftitle, to_difftitle = self._diff( from_version.title, to_version.title) from_authors, to_authors = self._diff( from_version.authors, to_version.authors) diff = difflib._mdiff(from_text, to_text) self.append(html.div(html.table( html.tr(html.th('Title'), html.td(html.a(from_difftitle, href=from_version.url)), html.td(html.a(to_difftitle, href=to_version.url)), **{'class': 'textdiff'}), html.tr(html.th('Authors'), html.td(from_authors), html.td(to_authors), **{'class': 'textdiff'}), html.tr(html.th('Date'), html.td(from_version.created_date.strftime('%x %X')), html.td(to_version.created_date.strftime('%x %X'))), html.tr(html.th('Text', colspan=3))), self._diff_lines(from_text, to_text), **{'class': 'versiondiff'} )) class ItemWidget(html.div, DiffSupport): def __init__(self, item): html.div.__init__(self) DiffSupport.__init__(self) self.append(html.h2(html.a(item.title, href=url_for('item', id=item.id)))) versionsFrom = [None] + list(item.versions)[:-1] versionsTo = item.versions versions = list(zip(versionsFrom, versionsTo)) self.append(html.ul( *map(self.version, versions))) def version(self, versions): from_version, to_version = versions if from_version == None: title = html.span(to_version.title) else: from_difftitle, to_difftitle = self._diff( from_version.title, to_version.title) title = html.span(to_difftitle, **{'class': 'textdiff'}) return html.li(html.span(to_version.created_date.strftime('%x %X')), html.span(' - '), title) class Template: @staticmethod def template(title, body): return xsc.Frag( html.DocTypeXHTML10transitional(), '\n', html.html( html.head( html.meta(charset='utf-8'), html.title(title), html.style(css.string( css.Rule('.textdiff', '.diff_add', backgroundColor='#CEF6CE'), css.Rule('.textdiff', '.diff_next', backgroundColor='#c0c0c0'), css.Rule('.textdiff', '.diff_sub', backgroundColor='#FFDADA'), css.Rule('.textdiff', '.diff_chg', backgroundColor='#E5E1FF') )) ), '\n', html.body(body))) @staticmethod def index(feeds): return Template.template( 'Index', html.ul( *map(lambda feed: html.li( html.a(feed.title or feed.url, href=url_for('feed', id=feed.id))), feeds))) @staticmethod def feed(feed, items): return Template.template( 'Feed {}'.format(feed.title or feed.url), html.div( html.a('Back', href=url_for('index')), html.h1(feed.title), *map(ItemWidget, items))) @staticmethod def item(item, versions): versionsA = versions versionsB = [None] + versions[:-1] versions = list(zip(versionsA, versionsB)) return Template.template( 'Item: {}'.format(item.title), html.div( html.a('Back to {}'.format(item.feed.title), href=url_for('feed', id=item.feed.id)), html.h1(item.title), *map(lambda versionAB: html.div( Difftable(versionAB[0], versionAB[1]), html.hr()), versions))) def get_app(): app = Flask(__name__) @app.route('/') def index(): return Template.index(data.Feed.select()).string('utf-8') @app.route('/feed/') def feed(id): feed = data.Feed.get(data.Feed.id == id) counts = (data.Version.select(data.Version.item.alias('item_id'), pw.fn.Count(data.Version.id).alias('count')) .group_by(data.Version.item) .alias('counts')) items = (data.Item.select() .join(counts, on=(data.Item.id == counts.c.item_id)) .where((data.Item.feed == feed) & (counts.c.count > 1)) .order_by(data.Item.created_date.desc())) return Template.feed(feed, items).string('utf-8') @app.route('/item/') def item(id): item = data.Item.get(data.Item.id == id) versions = data.Version.select() \ .where(data.Version.item == item) \ .order_by(data.Version.created_date) return Template.item(item, list(versions)).string('utf-8') return app