diff --git a/isso/__init__.py b/isso/__init__.py index 853187c..6f39171 100644 --- a/isso/__init__.py +++ b/isso/__init__.py @@ -54,7 +54,7 @@ from werkzeug.contrib.fixers import ProxyFix from jinja2 import Environment, FileSystemLoader -from isso import db, migrate, views, wsgi, notify, colors +from isso import db, migrate, views, wsgi, notify, colors, utils from isso.views import comment, admin url_map = Map([ @@ -62,16 +62,20 @@ url_map = Map([ Rule('/id/', methods=['GET', 'PUT', 'DELETE'], endpoint=views.comment.single), Rule('/id//like', methods=['POST'], endpoint=views.comment.like), + Rule('/id//dislike', methods=['POST'], endpoint=views.comment.dislike), Rule('/', methods=['GET'], endpoint=views.comment.fetch), Rule('/count', methods=['GET'], endpoint=views.comment.count), - Rule('/admin/', endpoint=views.admin.index) + Rule('/admin/', endpoint=views.admin.index), + + Rule('/check-ip', endpoint=views.comment.checkip) ]) class Isso(object): PRODUCTION = False + SALT = "Eech7co8Ohloopo9Ol6baimi" def __init__(self, dbpath, secret, origin, max_age, passphrase, mailer): diff --git a/isso/crypto.py b/isso/crypto.py new file mode 100644 index 0000000..8ebd6f6 --- /dev/null +++ b/isso/crypto.py @@ -0,0 +1,89 @@ +# -*- encoding: utf-8 -*- +# +# Copyright (c) Django Software Foundation and individual contributors. +# All rights reserved. + +import hmac +import time +import struct +import base64 +import hashlib +import binascii +import operator + + +def _bin_to_long(x): + """ + Convert a binary string into a long integer + + This is a clever optimization for fast xor vector math + """ + return int(binascii.hexlify(x), 16) + + +def _long_to_bin(x, hex_format_string): + """ + Convert a long integer into a binary string. + hex_format_string is like "%020x" for padding 10 characters. + """ + return binascii.unhexlify((hex_format_string % x).encode('ascii')) + + +def _fast_hmac(key, msg, digest): + """ + A trimmed down version of Python's HMAC implementation. + + This function operates on bytes. + """ + dig1, dig2 = digest(), digest() + if len(key) > dig1.block_size: + key = digest(key).digest() + key += b'\x00' * (dig1.block_size - len(key)) + dig1.update(key.translate(hmac.trans_36)) + dig1.update(msg) + dig2.update(key.translate(hmac.trans_5C)) + dig2.update(dig1.digest()) + return dig2 + + +def _pbkdf2(password, salt, iterations, dklen=0, digest=None): + """ + Implements PBKDF2 as defined in RFC 2898, section 5.2 + + HMAC+SHA256 is used as the default pseudo random function. + + Right now 10,000 iterations is the recommended default which takes + 100ms on a 2.2Ghz Core 2 Duo. This is probably the bare minimum + for security given 1000 iterations was recommended in 2001. This + code is very well optimized for CPython and is only four times + slower than openssl's implementation. + """ + + assert iterations > 0 + if not digest: + digest = hashlib.sha1 + password = b'' + password + salt = b'' + salt + hlen = digest().digest_size + if not dklen: + dklen = hlen + if dklen > (2 ** 32 - 1) * hlen: + raise OverflowError('dklen too big') + l = -(-dklen // hlen) + r = dklen - (l - 1) * hlen + + hex_format_string = "%%0%ix" % (hlen * 2) + + def F(i): + def U(): + u = salt + struct.pack(b'>I', i) + for j in xrange(int(iterations)): + u = _fast_hmac(password, u, digest).digest() + yield _bin_to_long(u) + return _long_to_bin(reduce(operator.xor, U()), hex_format_string) + + T = [F(x) for x in range(1, l + 1)] + return b''.join(T[:-1]) + T[-1][:r] + +pbkdf2 = lambda text, salt, iterations, dklen: base64.b16encode( + _pbkdf2(text.encode('utf-8'), salt, iterations, dklen)).lower() diff --git a/isso/db/comments.py b/isso/db/comments.py index 25b5466..0be5156 100644 --- a/isso/db/comments.py +++ b/isso/db/comments.py @@ -141,7 +141,7 @@ class Comments: self._remove_stale() return self.get(id) - def like(self, id, remote_addr): + def vote(self, upvote, id, remote_addr): """+1 a given comment. Returns the new like count (may not change because the creater can't vote on his/her own comment and multiple votes from the same ip address are ignored as well).""" @@ -151,23 +151,26 @@ class Comments: .fetchone() if rv is None: - return 0 + return None likes, dislikes, voters = rv if likes + dislikes >= 142: - return likes + return {'likes': likes, 'dislikes': dislikes} bf = Bloomfilter(bytearray(voters), likes + dislikes) if remote_addr in bf: - return likes + return {'likes': likes, 'dislikes': dislikes} bf.add(remote_addr) self.db.execute([ 'UPDATE comments SET', - ' likes = likes + 1, voters = ?', + ' likes = likes + 1,' if upvote else 'dislikes = dislikes + 1,', + ' voters = ?' 'WHERE id=?;'], (buffer(bf.array), id)) - return likes + 1 + if upvote: + return {'likes': likes + 1, 'dislikes': dislikes} + return {'likes': likes, 'dislikes': dislikes + 1} def count(self, uri): """ diff --git a/isso/js/app/api.js b/isso/js/app/api.js index 3b7f712..bd6547a 100644 --- a/isso/js/app/api.js +++ b/isso/js/app/api.js @@ -5,11 +5,14 @@ define(["lib/q"], function(Q) { + "use strict"; + // http://stackoverflow.com/questions/17544965/unhandled-rejection-reasons-should-be-empty - Q.stopUnhandledRejectionTracking(); +// Q.stopUnhandledRejectionTracking(); Q.longStackSupport = true; - var endpoint = null, + var endpoint = null, remote_addr = null, + salt = "Eech7co8Ohloopo9Ol6baimi", location = window.location.pathname; // guess Isso API location @@ -34,20 +37,17 @@ define(["lib/q"], function(Q) { var response = Q.defer(); if (! ("withCredentials" in xhr)) { - respone.reject("I won't support IE ≤ 10.") + respone.reject("I won't support IE ≤ 10."); return response.promise; } - xhr.withCredentials = true; - function onload() { response.resolve({status: xhr.status, body: xhr.responseText}); } try { xhr.open(method, url, true); - xhr.overrideMimeType("application/javascript"); - + xhr.withCredentials = true; // fuck you, fuck you, fuck you IE xhr.onreadystatechange = function () { if (xhr.readyState === 4) { onload(); @@ -62,56 +62,56 @@ define(["lib/q"], function(Q) { }; var qs = function(params) { - rv = ""; + var rv = ""; for (var key in params) { if (params.hasOwnProperty(key)) { rv += key + "=" + encodeURIComponent(params[key]) + "&"; } } - return rv.substring(0, rv.length - 1) // chop off trailing "&" + return rv.substring(0, rv.length - 1); // chop off trailing "&" } var create = function(data) { return curl("POST", endpoint + "/new?" + qs({uri: location}), JSON.stringify(data)) .then(function (rv) { - if (rv.status == 201 || rv.status == 202) { + if (rv.status === 201 || rv.status === 202) { return JSON.parse(rv.body); } else { - msg = rv.body.match("

(.+)

") - throw {status: rv.status, reason: (msg && msg[1]) || rv.body} + var msg = rv.body.match("

(.+)

"); + throw {status: rv.status, reason: (msg && msg[1]) || rv.body}; } - }) - } + }); + }; var modify = function(data) { // ... - } + }; var remove = function(id) { return curl("DELETE", endpoint + "/id/" + id, null) .then(function(rv) { - if (rv.status == 200) { - return JSON.parse(rv.body) == null; + if (rv.status === 200) { + return JSON.parse(rv.body) === null; } else { - throw {status: rv.status, reason: rv.body} + throw {status: rv.status, reason: rv.body}; } - }) - } + }); + }; - var fetchall = function() { + var fetch = function() { return curl("GET", endpoint + "/?" + qs({uri: location}), null) .then(function (rv) { - if (rv.status == 200) { - return JSON.parse(rv.body) + if (rv.status === 200) { + return JSON.parse(rv.body); } else { - msg = rv.body.match("

(.+)

") - throw {status: rv.status, reason: (msg && msg[1]) || rv.body} + var msg = rv.body.match("

(.+)

"); + throw {status: rv.status, reason: (msg && msg[1]) || rv.body}; } - }) - } + }); + }; var count = function(uri) { return curl("GET", endpoint + "/count?" + qs({uri: uri}), null) @@ -123,12 +123,30 @@ define(["lib/q"], function(Q) { }) } + var like = function(id) { + return curl("POST", endpoint + "/id/" + id + "/like", null) + .then(function(rv) { + return JSON.parse(rv.body); + }) + } + + var dislike = function(id) { + return curl("POST", endpoint + "/id/" + id + "/dislike", null) + .then(function(rv) { + return JSON.parse(rv.body); + }) + } + + remote_addr = curl("GET", endpoint + "/check-ip", null).then(function(rv) {return rv.body}); + return { - endpoint: endpoint, + endpoint: endpoint, remote_addr: remote_addr, salt: salt, create: create, remove: remove, - fetchall: fetchall, - count: count + fetch: fetch, + count: count, + like: like, + dislike: dislike } }); \ No newline at end of file diff --git a/isso/js/app/count.js b/isso/js/app/count.js index 57d2ce1..173dab5 100644 --- a/isso/js/app/count.js +++ b/isso/js/app/count.js @@ -1,15 +1,15 @@ -define(["app/api", "lib/HTML"], function(api, HTML) { +define(["app/api", "app/dom", "app/markup"], function(api, $, Mark) { return function() { - HTML.query("a").each(function(el, i, all) { + $.each("a", function(el) { if (! el.href.match("#isso-thread$")) { return; - }; + } var uri = el.href.match("^(.+)#isso-thread$")[1] .replace(/^.*\/\/[^\/]+/, ''); api.count(uri).then(function(rv) { - el.textContent = rv + (rv > 1 ? " Kommentare" : " Kommentar"); - }) + el.textContent = Mark.up("{{ i18n-num-comments | pluralize : `n` }}", {n: rv}); + }); }); - } + }; }); \ No newline at end of file diff --git a/isso/js/app/dom.js b/isso/js/app/dom.js new file mode 100644 index 0000000..9c9e54a --- /dev/null +++ b/isso/js/app/dom.js @@ -0,0 +1,82 @@ +define(function() { + + "use strict"; + + window.Element.prototype.replace = function(el) { + var element = DOM.htmlify(el); + this.parentNode.replaceChild(element, this); + return element; + }; + + window.Element.prototype.prepend = function(el) { + var element = DOM.htmlify(el); + this.insertBefore(element, this.firstChild); + return element; + }; + + window.Element.prototype.append = function(el) { + var element = DOM.htmlify(el); + this.appendChild(element); + return element; + }; + + window.Element.prototype.insertAfter = function(el) { + var element = DOM.htmlify(el); + this.parentNode.insertBefore(element, this.nextSibling); + return element; + }; + + window.Element.prototype.on = function(type, listener, prevent) { + this.addEventListener(type, function(event) { + listener(); + if (prevent === undefined || prevent) { + event.preventDefault(); + } + }); + }; + + window.Element.prototype.remove = function() { + // Mimimi, I am IE and I am so retarded, mimimi. + this.parentNode.removeChild(this); + }; + + var DOM = function(query, root) { + + if (! root) { + root = window.document; + } + + var elements = root.querySelectorAll(query); + + if (elements.length === 0) { + return null; + } + + if (elements.length === 1) { + return elements[0]; + } + + return elements; + }; + + DOM.htmlify = function(html) { + + if (html instanceof window.Element) { + return html; + } + + var wrapper = DOM.new("div"); + wrapper.innerHTML = html; + return wrapper.firstChild; + }; + + DOM.new = function(tag) { + return document.createElement(tag); + }; + + DOM.each = function(tag, func) { + Array.prototype.forEach.call(document.getElementsByTagName(tag), func); + }; + + return DOM; +}); \ No newline at end of file diff --git a/isso/js/app/forms.js b/isso/js/app/forms.js deleted file mode 100644 index 9f37565..0000000 --- a/isso/js/app/forms.js +++ /dev/null @@ -1,51 +0,0 @@ -define(["lib/HTML"], function(HTML) { - - var msgbox = function(defaults) { - - var form = document.createElement("div") - form.className = "isso-comment-box" - HTML.ify(form); - - var optional = form.add("ul.optional"); - optional.add("li>input[type=text name=author placeholder=Name ]").value = defaults.author || ""; - optional.add("li>input[type=email name=email placeholder=Email]").value = defaults.email || ""; - optional.add("li>input[type=url name=website placeholder=Website]").value = defaults.website || ""; - - var textarea = form.add("div>textarea[rows=2 name=text]"); - textarea.value = defaults.text || ""; - textarea.placeholder = "Kommentar hier eintippen (andere Felder sind optional)" - textarea.onfocus = function() { - textarea.rows = 8; - // scrollIntoView enhancement - }; - - form.add("input[type=submit]").value = "Kommentar hinzufügen"; - form.add("span"); - return form; - - } - - var validate = function(msgbox) { - if (msgbox.query("textarea").value.length < 3) { - msgbox.query("textarea").focus(); - msgbox.span.className = "isso-popup" - msgbox.span.innerHTML = "Dein Kommentar sollte schon etwas länger sein."; - msgbox.span.addEventListener("click", function(event) { - msgbox.span.className = ""; - msgbox.span.innerHTML = ""; - }) - setTimeout(function() { - msgbox.span.className = "" - msgbox.span.innerHTML = "" - }, 5000 ) - return false; - } - - return true; - } - - return { - msgbox: msgbox, - validate: validate - } -}); \ No newline at end of file diff --git a/isso/js/app/i18n.js b/isso/js/app/i18n.js new file mode 100644 index 0000000..6ed8278 --- /dev/null +++ b/isso/js/app/i18n.js @@ -0,0 +1,70 @@ +define(function() { + + "use strict"; + + // pluralization functions for each language you support + var plurals = { + "en": function (msgs, n) { + return msgs[n === 1 ? 0 : 1]; + } + }; + + plurals["de"] = plurals["en"]; + + // the user's language. you can replace this with your own code + var lang = (navigator.language || navigator.userLanguage).split("-")[0]; + + // fall back to English + if (!plurals[lang]) { + lang = "en"; + } + + return { + plurals: plurals, + lang: lang, + de: { + "postbox-text" : "Kommentar hier eintippen (mindestens 3 Zeichen)", + "postbox-author" : "Name (optional)", + "postbox-email" : "Email (optional)", + "postbox-submit": "Abschicken.", + + "num-comments": "1 Kommentar\n{{ n }} Kommentare", + "no-comments": "Keine Kommentare bis jetzt", + + "comment-reply": "Antworten", + "comment-delete": "Löschen", + "comment-confirm": "Bestätigen", + "comment-close": "Schließen", + + "date-now": "eben jetzt", + "date-minute": "vor einer Minute\nvor {{ n }} Minuten", + "date-hour": "vor einer Stunde\nvor {{ n }} Stunden", + "date-day": "Gestern\nvor {{ n }} Tagen", + "date-week": "letzte Woche\nvor {{ n }} Wochen", + "date-month": "letzten Monat\nvor {{ n }} Monaten", + "date-year": "letztes Jahr\nvor {{ n }} Jahren" + }, + en: { + "postbox-text": "Type Comment Here (at least 3 chars)", + "postbox-author": "Name (optional)", + "postbox-email": "E-mail (optional)", + "postbox-submit": "Post Comment.", + + "num-comments": "One Comment\n{{ n }} Comments", + "no-comments": "No Comments Yet", + + "comment-reply": "Reply", + "comment-delete": "Delete", + "comment-confirm": "Confirm", + "comment-close": "Close", + + "date-now": "now", + "date-minute": "a minute ago\n{{ n }} minutes ago", + "date-hour": "an hour ago\n{{ n }} hours ago", + "date-day": "Yesterday\n{{ n }} days ago", + "date-week": "last week\n{{ n }} weeks ago", + "date-month": "last month\n{{ n }} months ago", + "date-year": "last year\n{{ n }} years ago" + } + }; +}); diff --git a/isso/js/app/isso.js b/isso/js/app/isso.js index b56b8b5..77b7a69 100644 --- a/isso/js/app/isso.js +++ b/isso/js/app/isso.js @@ -5,172 +5,224 @@ */ -define(["lib/q", "lib/HTML", "helper/utils", "helper/identicons", "./api", "./forms"], function(Q, HTML, utils, identicons, api, forms) { +define(["lib/pbkdf2", "lib/identicons", "text/html", "./dom", "./utils", "./api", "./markup", "./i18n"], + function(pbkdf2, identicons, templates, $, utils, api, Mark, i18n) { + + "use strict"; + + var msgs = i18n[i18n.lang]; + + var toggle = function(el, on, off) { + if (el.classList.contains("off") || ! el.classList.contains("on")) { + el.classList.remove("off"); + el.classList.add("on"); + on(el); + } else { + el.classList.remove("on"); + el.classList.add("off"); + off(el); + } + }; + + var Postbox = function(parent) { + + var el = $.htmlify(Mark.up(templates["postbox"])); + + // add a blank identicon to not waste CPU cycles + // XXX show a space invader instead :> + $(".avatar > canvas", el).replace(identicons.blank(48, 48)); + + // on text area focus, generate identicon from IP address + $(".textarea-wrapper > textarea", el).on("focus", function() { + if ($(".avatar canvas", el).classList.contains("blank")) { + $(".avatar canvas", el).replace( + identicons.generate(pbkdf2(api.remote_addr, api.salt, 1000, 6), 48, 48)); + } + }); + + // update identicon, when the user provices an email address + var active; + $(".input-wrapper > [type=email]", el).on("keyup", function() { + if (active) { + clearTimeout(active); + } + active = setTimeout(function() { + pbkdf2($(".input-wrapper > [type=email]", el).value || api.remote_addr, api.salt, 1000, 6) + .then(function(rv) { + $(".avatar canvas", el).replace(identicons.generate(rv, 48, 48)); + }); + }, 200); + }, false); + + $(".input-wrapper > [type=email]", el).on("keydown", function() { + clearTimeout(active); + }, false); + + el.validate = function() { + if ($("textarea", this).value.length < 3) { + $("textarea", this).focus(); + return false; + } + return true; + }; + + $("[type=submit]", el).on("click", function() { + if (! el.validate()) { + return; + } + + api.create({ + author: $("[name=author]", el).value || null, + email: $("[name=email]", el).value || null, + text: $("textarea", el).value, + parent: parent || null + }).then(function(comment) { + $("[name=author]", el).value = ""; + $("[name=email]", el).value = ""; + $("textarea", el).value = ""; + $("textarea", el).rows = 2; + $("textarea", el).blur(); + insert(comment, true); + + if (parent !== null) { + el.remove(); + } + }); + }); + + return el; + }; + + var map = {id: {}, name: {}}; var insert = function(comment, scrollIntoView) { - /* - * insert a comment (JSON/object) into the #isso-thread or below a parent (#isso-N), renders some HTML and - * registers events to reply to, edit and remove a comment. - */ + map.name[comment.id] = comment.author; if (comment.parent) { - entrypoint = HTML.query("#isso-" + comment.parent).add("div.isso-follow-up"); - } else { - entrypoint = HTML.query("div#isso-root") + comment["replyto"] = map.name[comment.parent]; } - entrypoint.add("article.isso-comment#isso-" + comment.id) - .add("header+span.avatar+div.text+footer") - var node = HTML.query("#isso-" + comment.id), - date = new Date(parseInt(comment.created) * 1000); + var el = $.htmlify(Mark.up(templates["comment"], comment)); - if (comment.mode == 2) { - node.header.add("span.note").textContent = 'Kommentar muss noch freigeschaltet werden'; - } else if (comment.mode == 4) { // deleted - node.classList.add('deleted'); - node.header.add("span.note").textContent = "Kommentar gelöscht." - } - - if (comment.website) { - var el = node.header.add("a.author") - el.textContent= comment.author || 'Anonymous'; - el.href = comment.website; - el.rel = "nofollow" - } else { - node.header.add("span.author").innerHTML = comment.author || 'Anonymous'; - } - - node.header.add("span.spacer").textContent = "•"; - - var permalink = node.header.add("a.permalink"); - permalink.href = '#isso-' + comment.id; - permalink.add("date[datetime=" + date.getUTCFullYear() + "-" + date.getUTCMonth() + "-" + date.getUTCDay() + "]") var refresh = function() { - permalink.date.textContent = utils.ago(date); - setTimeout(refresh, 60*1000) + $(".permalink > date", el).textContent = utils.ago(new Date(parseInt(comment.created, 10) * 1000)); + setTimeout(refresh, 60*1000); }; refresh(); - var canvas = node.query("span.avatar").add("canvas[hash=" + comment.hash + "]"); - canvas.width = canvas.height = 48; - identicons.generate(canvas.getContext('2d'), comment.hash); + $("div.avatar > canvas", el).replace(identicons.generate(comment.hash, 48, 48)); - if (comment.mode == 4) { - node.query(".text").add("p").value = " " + var entrypoint; + if (comment.parent === null) { + entrypoint = $("#isso-root"); } else { - node.query(".text").innerHTML = comment.text; + var key = comment.parent; + while (key in map.id) { + key = map.id[key]; + } + map.id[comment.id] = comment.parent; + entrypoint = $("#isso-" + key + " > .text-wrapper > .isso-follow-up"); } - node.footer.add("a.liek{Liek}").href = "#"; - node.footer.add("a.reply{Antworten}").href = "#"; + entrypoint.append(el); if (scrollIntoView) { - node.scrollIntoView(false); + el.scrollIntoView(); } - if (utils.read(comment.id)) { - node.footer.add("a.delete{Löschen}").href = "#"; - node.footer.add("a.edit{Bearbeiten}").href = "#"; + var footer = $("#isso-" + comment.id + " > .text-wrapper > footer"), + header = $("#isso-" + comment.id + " > .text-wrapper > header"); - var delbtn = node.query("a.delete"), - editbtn = node.query("a.edit"); - - delbtn.addEventListener("click", function(event) { - if (delbtn.textContent == "Bestätigen") { - api.remove(comment.id).then(function(rv) { - if (rv) { - node.remove(); - } else { - node.classList.add('deleted'); - node.header.add("span.note").textContent = "Kommentar gelöscht."; - HTML.query("#isso-" + comment.id + " > div.text").innerHTML = "

 

" - } - }) - } else { - delbtn.textContent = "Bestätigen" - setTimeout(function() {delbtn.textContent = "Löschen"}, 1500) + var form = new Postbox(comment.id); + $("a.reply", footer).on("click", function() { + toggle( + $("a.reply", footer), + function(reply) { + footer.insertAfter(form); + reply.textContent = msgs["comment-close"]; + }, + function(reply) { + form.remove(); + reply.textContent = msgs["comment-reply"]; } - event.preventDefault(); - }) + ); + }); + + if (comment.parent !== null) { + $("a.parent", header).on("mouseover", function() { + $("#isso-" + comment.parent).classList.add("parent-highlight"); + }); + $("a.parent", header).on("mouseout", function() { + $("#isso-" + comment.parent).classList.remove("parent-highlight"); + }); } - // ability to answer directly to a comment - HTML.query("#isso-" + comment.id + " a.reply").addEventListener("click", function(event) { - - // remove active form when clicked again or reply to another comment - var active = HTML.query(".isso-active-msgbox"); // [] when empty, element if not - - if (! (active instanceof Array)) { - active.query("div.isso-comment-box").remove() - active.classList.remove("isso-active-msgbox"); - active.query("a.reply").textContent = "Antworten" - - if (active.id == "isso-" + comment.id) { - event.preventDefault(); - return; - } + var votes = function (value) { + var span = $("span.votes", footer); + if (span === null) { + if (value === 0) { + span.remove(); + return; + } else { + footer.prepend($.htmlify('' + value + '')); + } + } else { + if (value === 0) { + span.remove(); + } else { + span.textContent = value; + } } + }; - var msgbox = forms.msgbox({}) - HTML.query("#isso-" + comment.id).footer.appendChild(msgbox); - HTML.query("#isso-" + comment.id).classList.add("isso-active-msgbox"); - HTML.query("#isso-" + comment.id + " a.reply").textContent = "Schließen"; - - // msgbox.scrollIntoView(false); - msgbox.query("input[type=submit]").addEventListener("click", function(event) { - forms.validate(msgbox) && api.create({ - author: msgbox.query("[name=author]").value || null, - email: msgbox.query("[name=email]").value || null, - website: msgbox.query("[name=website]").value || null, - text: msgbox.query("textarea").value, - parent: comment.id }) - .then(function(rv) { - // remove box on submit - msgbox.parentNode.parentNode.classList.remove("isso-active-msgbox"); - msgbox.parentNode.parentNode.query("a.reply").textContent = "Antworten" - msgbox.remove() - insert(rv, true); - }) - event.preventDefault() + $("a.upvote", footer).on("click", function() { + api.like(comment.id).then(function(rv) { + votes(rv.likes - rv.dislikes); }); - event.preventDefault(); - }); - } - - var init = function() { - - var rootmsgbox = forms.msgbox({}); - var h4 = HTML.query("#isso-thread").add("h4") - HTML.query("#isso-thread").add("div#isso-root").add(rootmsgbox); - rootmsgbox.query("input[type=submit]").addEventListener("click", function(event) { - forms.validate(rootmsgbox) && api.create({ - author: rootmsgbox.query("[name=author]").value || null, - email: rootmsgbox.query("[name=email]").value || null, - website: rootmsgbox.query("[name=website]").value || null, - text: rootmsgbox.query("textarea").value, - parent: null }) - .then(function(rv) { - rootmsgbox.query("[name=author]").value = ""; - rootmsgbox.query("[name=email]").value = ""; - rootmsgbox.query("[name=website]").value = ""; - rootmsgbox.query("textarea").value = ""; - rootmsgbox.query("textarea").rows = 2; - rootmsgbox.query("textarea").blur(); - insert(rv, true); - }) - event.preventDefault() }); - api.fetchall().then(function(comments) { - h4.textContent = comments.length + " Kommentare zu \"" + utils.heading() + "\""; - for (var i in comments) { - insert(comments[i]) + $("a.downvote", footer).on("click", function() { + api.dislike(comment.id).then(function(rv) { + votes(rv.likes - rv.dislikes); + }); + }); + + if (! utils.cookie(comment.id)) { +// $("a.edit", footer).remove(); + $("a.delete", footer).remove(); + return; + } + + $("a.delete", footer).on("click", function() { + if ($("a.delete", footer).textContent === msgs["comment-confirm"]) { + api.remove(comment.id).then(function(rv) { + if (rv) { + el.remove(); + } else { + $("span.note", el).textContent = "Kommentar gelöscht."; + $(".text", el).innerHTML = "

 

"; + } + }); + } else { + $("a.delete", footer).textContent = msgs["comment-confirm"]; + setTimeout(function() { + $("a.delete", footer).textContent = msgs["comment-delete"]; + }, 1500); } - }).fail(function(rv) { - h4.textContent = "Kommentiere \"" + utils.heading() + "\""; - }) - } + }); + + return; + +// var votes = node.footer.add("span.votes{" + (comment.likes - comment.dislikes) + "}"); +// node.footer.add(forms.like(comment.id, function(rv) { +// votes.textContent = rv.likes - rv.dislikes; +// })) +// node.footer.add(forms.dislike(comment.id, function(rv) { +// votes.textContent = rv.likes - rv.dislikes; +// })) + }; return { - init: init - } + insert: insert, + Postbox: Postbox + }; }); \ No newline at end of file diff --git a/isso/js/app/markup.js b/isso/js/app/markup.js new file mode 100644 index 0000000..f9b4eb9 --- /dev/null +++ b/isso/js/app/markup.js @@ -0,0 +1,58 @@ +define(["lib/markup", "./i18n", "text/svg"], function(Mark, i18n, svg) { + + "use strict"; + + var pad = function(n, width, z) { + z = z || '0'; + n = n + ''; + return n.length >= width ? n : new Array(width - n.length + 1).join(z) + n; + }; + + // circumvent https://github.com/adammark/Markup.js/issues/22 + function merge(obj) { + var result = {}; + for (var prefix in obj) { + for (var attrname in obj[prefix]) { + result[prefix + "-" + attrname] = obj[prefix][attrname]; + } + } + return result; + } + + Mark.delimiter = ":"; + Mark.includes = merge({ + i18n: i18n[i18n.lang], + svg: svg + }); + + Mark.pipes.datetime = function(date) { + if (typeof date !== "object") { + date = new Date(parseInt(date, 10) * 1000); + } + + return [date.getUTCFullYear(), pad(date.getUTCMonth(), 2), pad(date.getUTCDay(), 2)].join("-"); + }; + + Mark.pipes.substract = function(a, b) { + return parseInt(a, 10) - parseInt(b, 10); + }; + + Mark.pipes.pluralize = function(str, n) { + return i18n.plurals[i18n.lang](str.split("\n"), +n).trim(); + }; + + var strip = function(string) { + // allow whitespace between Markup.js delimiters such as + // {{ var | pipe : arg }} instead of {{var|pipe:arg}} + return string.replace(/\{\{\s*(.+?)\s*\}\}/g, function(match, val) { + return ("{{" + val + "}}").replace(/\s*\|\s*/g, "|") + .replace(/\s*\:\s*/g, ":"); + }); + }; + + return { + up: function(template, context) { + return Mark.up(strip(template), context); + } + }; +}); \ No newline at end of file diff --git a/isso/js/app/utils.js b/isso/js/app/utils.js new file mode 100644 index 0000000..9abdbf2 --- /dev/null +++ b/isso/js/app/utils.js @@ -0,0 +1,45 @@ +/* Copyright 2013, Martin Zimmermann . All rights reserved. + * License: BSD Style, 2 clauses. See isso/__init__.py. + * + * utility functions + */ + +define(["./markup"], function(Mark) { + + // return `cookie` string if set + var cookie = function(cookie) { + return (document.cookie.match('(^|; )' + cookie + '=([^;]*)') || 0)[2]; + }; + + var ago = function(date) { + + var diff = (((new Date()).getTime() - date.getTime()) / 1000), + day_diff = Math.floor(diff / 86400); + + if (isNaN(day_diff) || day_diff < 0) { + return; + } + + var i18n = function(msgid, n) { + if (! n) { + return Mark.up("{{ i18n-" + msgid + " }}"); + } else { + return Mark.up("{{ i18n-" + msgid + " | pluralize : `n` }}", {n: n}); + } + }; + + return day_diff === 0 && ( + diff < 60 && i18n("date-now") || + diff < 3600 && i18n("date-minute", Math.floor(diff / 60)) || + diff < 86400 && i18n("date-hour", Math.floor(diff / 3600))) || + day_diff === 1 && i18n("date-day", day_diff) || + day_diff < 31 && i18n("date-week", Math.ceil(day_diff / 7)) || + day_diff < 365 && i18n("date-month", Math.ceil(day_diff / 30)) || + i18n("date-year", Math.ceil(day_diff / 365.25)); + }; + + return { + cookie: cookie, + ago: ago + }; +}); \ No newline at end of file diff --git a/isso/js/build.embed.js b/isso/js/build.embed.js index 1d16e1f..6dd1fd4 100644 --- a/isso/js/build.embed.js +++ b/isso/js/build.embed.js @@ -3,5 +3,7 @@ name: "lib/almond", include: ['embed'], out: "embed.min.js", + + optimizeAllPluginResources: true, wrap: true }) diff --git a/isso/js/embed.js b/isso/js/embed.js index 50eec5d..940055e 100644 --- a/isso/js/embed.js +++ b/isso/js/embed.js @@ -1,6 +1,21 @@ -require(["lib/ready", "app/isso", "app/count"], function(domready, isso, count) { +require(["lib/ready", "app/api", "app/isso", "app/count", "app/dom", "app/markup"], function(domready, api, isso, count, $, Mark) { + + "use strict"; + domready(function() { count(); - isso.init(); - }) -}); + + $("#isso-thread").append($.new('h4')); + $("#isso-thread").append(new isso.Postbox(null)); + $("#isso-thread").append('
'); + + api.fetch().then(function(comments) { + $("#isso-thread > h4").textContent = Mark.up("{{ i18n-num-comments | pluralize : `n` }}", {n: comments.length}); + for (var i=0; i < comments.length; i++) { + isso.insert(comments[i], false); + } + }).fail(function() { + $("#isso-thread > h4").textContent = Mark.up("{{ i18n-no-comments }}"); + }); + }); +}); \ No newline at end of file diff --git a/isso/js/helper/utils.js b/isso/js/helper/utils.js deleted file mode 100644 index ea00a65..0000000 --- a/isso/js/helper/utils.js +++ /dev/null @@ -1,83 +0,0 @@ -/* Copyright 2013, Martin Zimmermann . All rights reserved. - * License: BSD Style, 2 clauses. See isso/__init__.py. - * - * utility functions - */ - -define({ - - // return `cookie` string if set - read: function(cookie) { - return (document.cookie.match('(^|; )' + cookie + '=([^;]*)') || 0)[2] - }, - - ago: function(date) { - /*! - * JavaScript Pretty Date - * Copyright (c) 2011 John Resig (ejohn.org) - * Licensed under the MIT and GPL licenses. - */ - var diff = (((new Date()).getTime() - date.getTime()) / 1000), - day_diff = Math.floor(diff / 86400); - - if (isNaN(day_diff) || day_diff < 0) - return; - - return day_diff == 0 && ( - diff < 60 && "eben jetzt" || - diff < 120 && "vor einer Minute" || - diff < 3600 && "vor " + Math.floor(diff / 60) + " Minuten" || - diff < 7200 && "vor einer Stunde" || - diff < 86400 && "vor " + Math.floor(diff / 3600) + " Stunden") || - day_diff == 1 && "Gestern" || - day_diff < 7 && "vor " + day_diff + " Tagen" || - day_diff < 31 && "vor " + Math.ceil(day_diff / 7) + " Wochen" || - day_diff < 365 && "vor " + Math.ceil(day_diff / 30) + " Monaten" || - "vor " + Math.ceil(day_diff / 365.25) + " Jahren"; - }, - - heading: function() { - /* - * return first level heading that is probably the - * blog title. If no h1 is found, "Untitled." is used. - */ - var el = document.getElementById("isso-thread"); - var visited = []; - - var recurse = function(node) { - for (var i = 0; i < node.childNodes.length; i++) { - var child = node.childNodes[i]; - - if (child.nodeType != child.ELEMENT_NODE) { - continue; - } - - if (child.nodeName == "H1") { - return child; - } - - if (visited.indexOf(child) == -1) { - return recurse(child); - } - } - } - - while (el != null) { - - visited.push(el); - - if (el == document.documentElement) { - break - } - - var rv = recurse(el); - if (rv) { - return rv.textContent.trim(); - } - - el = el.parentNode; - } - - return "Untitled." - } -}); \ No newline at end of file diff --git a/isso/js/helper/identicons.js b/isso/js/lib/identicons.js similarity index 52% rename from isso/js/helper/identicons.js rename to isso/js/lib/identicons.js index d372ed6..b8a2652 100644 --- a/isso/js/helper/identicons.js +++ b/isso/js/lib/identicons.js @@ -1,7 +1,9 @@ -define(function() { +define(["lib/q"], function(Q) { + + "use strict"; // JS Identicon generation via Gregory Schier (http://codepen.io/gschier/pen/GLvAy) - // modified to work with a given seed using Jenkins hashing. + // extended to produce the same identicon for a given hash // Size of a grid square in pixels var SQUARE = 8; @@ -10,28 +12,11 @@ define(function() { var GRID = 5; // Padding on the edge of the canvas in px - var PADDING = SQUARE/2; - - /* Jenkins 18-bit hash */ - var jenkins = function(key) { - var hash = 0; - - for (var i=0; i> 6); - } - - hash += (hash << 3); - hash ^= (hash >> 11); - hash += (hash << 15); - - return (hash >>> 0) % Math.pow(2, 18); - } + var PADDING = 4; var pad = function(n, width) { return n.length >= width ? n : new Array(width - n.length + 1).join("0") + n; - } + }; /** * Fill in a square on the canvas. @@ -51,13 +36,31 @@ define(function() { /** * Pick random squares to fill in. */ - var generateIdenticon = function(ctx, key) { + var generateIdenticon = function(key, height, width) { - var hash = pad(jenkins(key).toString(2), 18), - index = 0, color = null; + var canvas = document.createElement("canvas"), + ctx = canvas.getContext("2d"); + canvas.width = width; + canvas.height = height; - // via http://colrd.com/palette/19308/ - switch (hash.substring(hash.length - 3, hash.length)) { + // FILL CANVAS BG + ctx.beginPath(); + ctx.rect(0, 0, height, width); + ctx.fillStyle = '#F0F0F0'; + ctx.fill(); + + if (typeof key === null) { + return canvas; + } + + Q.when(key, function(key) { + var hash = pad((parseInt(key, 16) % Math.pow(2, 18)).toString(2), 18), + index = 0, color = null; + + canvas.setAttribute("data-hash", key); + + // via http://colrd.com/palette/19308/ + switch (hash.substring(hash.length - 3, hash.length)) { case "000": color = "#9abf88"; break; @@ -82,35 +85,36 @@ define(function() { case "111": color = "#447c69"; break; - } - - // FILL CANVAS BG - ctx.beginPath(); - ctx.rect(0, 0, 48, 48); - ctx.fillStyle = '#F0F0F0'; - ctx.fill(); - - // FILL THE SQUARES - for (var x=0; xred}}. + delimiter: ">", + + // Collapse white space between HTML elements in the resulting string. + compact: false, + + // Shallow-copy an object. + _copy: function (a, b) { + b = b || []; + + for (var i in a) { + b[i] = a[i]; + } + + return b; + }, + + // Get the value of a number or size of an array. This is a helper function for several pipes. + _size: function (a) { + return a instanceof Array ? a.length : (a || 0); + }, + + // This object represents an iteration. It has an index and length. + _iter: function (idx, size) { + this.idx = idx; + this.size = size; + this.length = size; + this.sign = "#"; + + // Print the index if "#" or the count if "##". + this.toString = function () { + return this.idx + this.sign.length - 1; + }; + }, + + // Pass a value through a series of pipe expressions, e.g. _pipe(123, ["add>10","times>5"]). + _pipe: function (val, expressions) { + var expression, parts, fn, result; + + // If we have expressions, pull out the first one, e.g. "add>10". + if ((expression = expressions.shift())) { + + // Split the expression into its component parts, e.g. ["add", "10"]. + parts = expression.split(this.delimiter); + + // Pull out the function name, e.g. "add". + fn = parts.shift().trim(); + + try { + // Run the function, e.g. add(123, 10) ... + result = Mark.pipes[fn].apply(null, [val].concat(parts)); + + // ... then pipe again with remaining expressions. + val = this._pipe(result, expressions); + } + catch (e) { + } + } + + // Return the piped value. + return val; + }, + + // TODO doc + _eval: function (context, filters, child) { + var result = this._pipe(context, filters), + ctx = result, + i = -1, + j, + opts; + + if (result instanceof Array) { + result = ""; + j = ctx.length; + + while (++i < j) { + opts = { + iter: new this._iter(i, j) + }; + result += child ? Mark.up(child, ctx[i], opts) : ctx[i]; + } + } + else if (result instanceof Object) { + result = Mark.up(child, ctx); + } + + return result; + }, + + // Process the contents of an IF or IF/ELSE block. + _test: function (bool, child, context, options) { + // Process the child string, then split it into the IF and ELSE parts. + var str = Mark.up(child, context, options).split(/\{\{\s*else\s*\}\}/); + + // Return the IF or ELSE part. If no ELSE, return an empty string. + return (bool === false ? str[1] : str[0]) || ""; + }, + + // Determine the extent of a block expression, e.g. "{{foo}}...{{/foo}}" + _bridge: function (tpl, tkn) { + var exp = "{{\\s*" + tkn + "([^/}]+\\w*)?}}|{{/" + tkn + "\\s*}}", + re = new RegExp(exp, "g"), + tags = tpl.match(re) || [], + t, + i, + a = 0, + b = 0, + c = -1, + d = 0; + + for (i = 0; i < tags.length; i++) { + t = i; + c = tpl.indexOf(tags[t], c + 1); + + if (tags[t].indexOf("{{/") > -1) { + b++; + } + else { + a++; + } + + if (a === b) { + break; + } + } + + a = tpl.indexOf(tags[0]); + b = a + tags[0].length; + d = c + tags[t].length; + + // Return the block, e.g. "{{foo}}bar{{/foo}}" and its child, e.g. "bar". + return [tpl.substring(a, d), tpl.substring(b, c)]; + } +}; + +// Inject a template string with contextual data and return a new string. +Mark.up = function (template, context, options) { + context = context || {}; + options = options || {}; + + // Match all tags like "{{...}}". + var re = /\{\{(.+?)\}\}/g, + // All tags in the template. + tags = template.match(re) || [], + // The tag being evaluated, e.g. "{{hamster|dance}}". + tag, + // The expression to evaluate inside the tag, e.g. "hamster|dance". + prop, + // The token itself, e.g. "hamster". + token, + // An array of pipe expressions, e.g. ["more>1", "less>2"]. + filters = [], + // Does the tag close itself? e.g. "{{stuff/}}". + selfy, + // Is the tag an "if" statement? + testy, + // The contents of a block tag, e.g. "{{aa}}bb{{/aa}}" -> "bb". + child, + // The resulting string. + result, + // The global variable being evaluated, or undefined. + global, + // The included template being evaluated, or undefined. + include, + // A placeholder variable. + ctx, + // Iterators. + i = 0, + j = 0; + + // Set custom pipes, if provided. + if (options.pipes) { + this._copy(options.pipes, this.pipes); + } + + // Set templates to include, if provided. + if (options.includes) { + this._copy(options.includes, this.includes); + } + + // Set global variables, if provided. + if (options.globals) { + this._copy(options.globals, this.globals); + } + + // Optionally override the delimiter. + if (options.delimiter) { + this.delimiter = options.delimiter; + } + + // Optionally collapse white space. + if (options.compact !== undefined) { + this.compact = options.compact; + } + + // Loop through tags, e.g. {{a}}, {{b}}, {{c}}, {{/c}}. + while ((tag = tags[i++])) { + result = undefined; + child = ""; + selfy = tag.indexOf("/}}") > -1; + prop = tag.substr(2, tag.length - (selfy ? 5 : 4)); + prop = prop.replace(/`(.+?)`/g, function (s, p1) { + return Mark.up("{{" + p1 + "}}", context); + }); + testy = prop.trim().indexOf("if ") === 0; + filters = prop.split("|"); + filters.shift(); // instead of splice(1) + prop = prop.replace(/^\s*if/, "").split("|").shift().trim(); + token = testy ? "if" : prop.split("|")[0]; + ctx = context[prop]; + + // If an "if" statement without filters, assume "{{if foo|notempty}}" + if (testy && !filters.length) { + filters = ["notempty"]; + } + + // Does the tag have a corresponding closing tag? If so, find it and move the cursor. + if (!selfy && template.indexOf("{{/" + token) > -1) { + result = this._bridge(template, token); + tag = result[0]; + child = result[1]; + i += tag.match(re).length - 1; // fast forward + } + + // Skip "else" tags. These are pulled out in _test(). + if (/^\{\{\s*else\s*\}\}$/.test(tag)) { + continue; + } + + // Evaluating a global variable. + else if ((global = this.globals[prop]) !== undefined) { + result = this._eval(global, filters, child); + } + + // Evaluating an included template. + else if ((include = this.includes[prop])) { + if (include instanceof Function) { + include = include(); + } + result = this._pipe(Mark.up(include, context), filters); + } + + // Evaluating a loop counter ("#" or "##"). + else if (prop.indexOf("#") > -1) { + options.iter.sign = prop; + result = this._pipe(options.iter, filters); + } + + // Evaluating the current context. + else if (prop === ".") { + result = this._pipe(context, filters); + } + + // Evaluating a variable with dot notation, e.g. "a.b.c" + else if (prop.indexOf(".") > -1) { + prop = prop.split("."); + ctx = Mark.globals[prop[0]]; + + if (ctx) { + j = 1; + } + else { + j = 0; + ctx = context; + } + + // Get the actual context + while (ctx && j < prop.length) { + ctx = ctx[prop[j++]]; + } + + result = this._eval(ctx, filters, child); + } + + // Evaluating an "if" statement. + else if (testy) { + result = this._pipe(ctx, filters); + } + + // Evaluating an array, which might be a block expression. + else if (ctx instanceof Array) { + result = this._eval(ctx, filters, child); + } + + // Evaluating a block expression. + else if (child) { + result = ctx ? Mark.up(child, ctx) : undefined; + } + + // Evaluating anything else. + else if (context.hasOwnProperty(prop)) { + result = this._pipe(ctx, filters); + } + + // Evaluating an "if" statement. + if (testy) { + result = this._test(result, child, context, options); + } + + // Replace the tag, e.g. "{{name}}", with the result, e.g. "Adam". + template = template.replace(tag, result === undefined ? "???" : result); + } + + return this.compact ? template.replace(/>\s+<") : template; +}; + +// Freebie pipes. See usage in README.md +Mark.pipes = { + empty: function (obj) { + return !obj || (obj + "").trim().length === 0 ? obj : false; + }, + notempty: function (obj) { + return obj && (obj + "").trim().length ? obj : false; + }, + blank: function (str, val) { + return !!str || str === 0 ? str : val; + }, + more: function (a, b) { + return Mark._size(a) > b ? a : false; + }, + less: function (a, b) { + return Mark._size(a) < b ? a : false; + }, + ormore: function (a, b) { + return Mark._size(a) >= b ? a : false; + }, + orless: function (a, b) { + return Mark._size(a) <= b ? a : false; + }, + between: function (a, b, c) { + a = Mark._size(a); + return a >= b && a <= c ? a : false; + }, + equals: function (a, b) { + return a == b ? a : false; + }, + notequals: function (a, b) { + return a != b ? a : false; + }, + like: function (str, pattern) { + return new RegExp(pattern, "i").test(str) ? str : false; + }, + notlike: function (str, pattern) { + return !Mark.pipes.like(str, pattern) ? str : false; + }, + upcase: function (str) { + return String(str).toUpperCase(); + }, + downcase: function (str) { + return String(str).toLowerCase(); + }, + capcase: function (str) { + return str.replace(/\b\w/g, function (s) { return s.toUpperCase(); }); + }, + chop: function (str, n) { + return str.length > n ? str.substr(0, n) + "..." : str; + }, + tease: function (str, n) { + var a = str.split(/\s+/); + return a.slice(0, n).join(" ") + (a.length > n ? "..." : ""); + }, + trim: function (str) { + return str.trim(); + }, + pack: function (str) { + return str.trim().replace(/\s{2,}/g, " "); + }, + round: function (num) { + return Math.round(+num); + }, + clean: function (str) { + return String(str).replace(/<\/?[^>]+>/gi, ""); + }, + size: function (obj) { + return obj.length; + }, + length: function (obj) { + return obj.length; + }, + reverse: function (arr) { + return Mark._copy(arr).reverse(); + }, + join: function (arr, separator) { + return arr.join(separator); + }, + limit: function (arr, count, idx) { + return arr.slice(+idx || 0, +count + (+idx || 0)); + }, + split: function (str, separator) { + return str.split(separator || ","); + }, + choose: function (bool, iffy, elsy) { + return !!bool ? iffy : (elsy || ""); + }, + toggle: function (obj, csv1, csv2, str) { + return csv2.split(",")[csv1.match(/\w+/g).indexOf(obj + "")] || str; + }, + sort: function (arr, prop) { + var fn = function (a, b) { + return a[prop] > b[prop] ? 1 : -1; + }; + return Mark._copy(arr).sort(prop ? fn : undefined); + }, + fix: function (num, n) { + return (+num).toFixed(n); + }, + mod: function (num, n) { + return (+num) % (+n); + }, + divisible: function (num, n) { + return num && (+num % n) === 0 ? num : false; + }, + even: function (num) { + return num && (+num & 1) === 0 ? num : false; + }, + odd: function (num) { + return num && (+num & 1) === 1 ? num : false; + }, + number: function (str) { + return parseFloat(str.replace(/[^\-\d\.]/g, "")); + }, + url: function (str) { + return encodeURI(str); + }, + bool: function (obj) { + return !!obj; + }, + falsy: function (obj) { + return !obj; + }, + first: function (iter) { + return iter.idx === 0; + }, + last: function (iter) { + return iter.idx === iter.size - 1; + }, + call: function (obj, fn) { + return obj[fn].apply(obj, [].slice.call(arguments, 2)); + }, + set: function (obj, key) { + Mark.globals[key] = obj; return ""; + }, + log: function (obj) { + console.log(obj); + return obj; + } +}; + +// Shim for IE. +if (typeof String.prototype.trim !== "function") { + String.prototype.trim = function() { + return this.replace(/^\s+|\s+$/g, ""); + } +} + +// Export for Node.js and AMD. +if (typeof module !== "undefined" && module.exports) { + module.exports = Mark; +} +else if (typeof define === "function" && define.amd) { + define(function() { + return Mark; + }); +} diff --git a/isso/js/lib/pbkdf2.js b/isso/js/lib/pbkdf2.js new file mode 100644 index 0000000..ed5c1a7 --- /dev/null +++ b/isso/js/lib/pbkdf2.js @@ -0,0 +1,201 @@ +define(["lib/q", "lib/sha1"], function(Q, sha1) { + /* + * JavaScript implementation of Password-Based Key Derivation Function 2 + * (PBKDF2) as defined in RFC 2898. + * Version 1.5 + * Copyright (c) 2007, 2008, 2009, 2010, 2011, 2012, 2013 Parvez Anandam + * parvez@anandam.com + * http://anandam.com/pbkdf2 + * + * Distributed under the BSD license + * + * Uses Paul Johnston's excellent SHA-1 JavaScript library sha1.js: + * http://pajhome.org.uk/crypt/md5/sha1.html + * (uses the binb_sha1(), rstr2binb(), binb2str(), rstr2hex() functions from that libary) + * + * Thanks to Felix Gartsman for pointing out a bug in version 1.0 + * Thanks to Thijs Van der Schaeghe for pointing out a bug in version 1.1 + * Thanks to Richard Gautier for asking to clarify dependencies in version 1.2 + * Updated contact information from version 1.3 + * Thanks to Stuart Heinrich for pointing out updates to PAJ's SHA-1 library in version 1.4 + */ + + + /* + * The four arguments to the constructor of the PBKDF2 object are + * the password, salt, number of iterations and number of bytes in + * generated key. This follows the RFC 2898 definition: PBKDF2 (P, S, c, dkLen) + * + * The method deriveKey takes two parameters, both callback functions: + * the first is used to provide status on the computation, the second + * is called with the result of the computation (the generated key in hex). + * + * Example of use: + * + * + * + * + *
+ * + */ + + var PBKDF2 = function(password, salt, num_iterations, num_bytes) + { + // Remember the password and salt + var m_bpassword = sha1.rstr2binb(password); + var m_salt = salt; + + // Total number of iterations + var m_total_iterations = num_iterations; + + // Run iterations in chunks instead of all at once, so as to not block. + // Define size of chunk here; adjust for slower or faster machines if necessary. + var m_iterations_in_chunk = 10; + + // Iteration counter + var m_iterations_done = 0; + + // Key length, as number of bytes + var m_key_length = num_bytes; + + // The hash cache + var m_hash = null; + + // The length (number of bytes) of the output of the pseudo-random function. + // Since HMAC-SHA1 is the standard, and what is used here, it's 20 bytes. + var m_hash_length = 20; + + // Number of hash-sized blocks in the derived key (called 'l' in RFC2898) + var m_total_blocks = Math.ceil(m_key_length/m_hash_length); + + // Start computation with the first block + var m_current_block = 1; + + // Used in the HMAC-SHA1 computations + var m_ipad = new Array(16); + var m_opad = new Array(16); + + // This is where the result of the iterations gets sotred + var m_buffer = new Array(0x0,0x0,0x0,0x0,0x0); + + // The result + var m_key = ""; + + // This object + var m_this_object = this; + + // The function to call with the result + var m_result_func; + + // The function to call with status after computing every chunk + var m_status_func; + + // Set up the HMAC-SHA1 computations + if (m_bpassword.length > 16) m_bpassword = sha1.binb_sha1(m_bpassword, password.length * chrsz); + for(var i = 0; i < 16; ++i) + { + m_ipad[i] = m_bpassword[i] ^ 0x36363636; + m_opad[i] = m_bpassword[i] ^ 0x5C5C5C5C; + } + + + // Starts the computation + this.deriveKey = function(status_callback, result_callback) + { + m_status_func = status_callback; + m_result_func = result_callback; + setTimeout(function() { m_this_object.do_PBKDF2_iterations() }, 0); + } + + + // The workhorse + this.do_PBKDF2_iterations = function() + { + var iterations = m_iterations_in_chunk; + if (m_total_iterations - m_iterations_done < m_iterations_in_chunk) + iterations = m_total_iterations - m_iterations_done; + + for(var i=0; i> 24 & 0xF) + + String.fromCharCode(m_current_block >> 16 & 0xF) + + String.fromCharCode(m_current_block >> 8 & 0xF) + + String.fromCharCode(m_current_block & 0xF); + + m_hash = sha1.binb_sha1(m_ipad.concat(sha1.rstr2binb(salt_block)), + 512 + salt_block.length * 8); + m_hash = sha1.binb_sha1(m_opad.concat(m_hash), 512 + 160); + } + else + { + m_hash = sha1.binb_sha1(m_ipad.concat(m_hash), + 512 + m_hash.length * 32); + m_hash = sha1.binb_sha1(m_opad.concat(m_hash), 512 + 160); + } + + for(var j=0; j 16) bkey = binb_sha1(bkey, key.length * 8); + + var ipad = Array(16), opad = Array(16); + for(var i = 0; i < 16; i++) + { + ipad[i] = bkey[i] ^ 0x36363636; + opad[i] = bkey[i] ^ 0x5C5C5C5C; + } + + var hash = binb_sha1(ipad.concat(rstr2binb(data)), 512 + data.length * 8); + return binb2rstr(binb_sha1(opad.concat(hash), 512 + 160)); + } + + /* + * Convert a raw string to a hex string + */ + function rstr2hex(input) + { + try { hexcase } catch(e) { hexcase=0; } + var hex_tab = hexcase ? "0123456789ABCDEF" : "0123456789abcdef"; + var output = ""; + var x; + for(var i = 0; i < input.length; i++) + { + x = input.charCodeAt(i); + output += hex_tab.charAt((x >>> 4) & 0x0F) + + hex_tab.charAt( x & 0x0F); + } + return output; + } + + /* + * Convert a raw string to a base-64 string + */ + function rstr2b64(input) + { + try { b64pad } catch(e) { b64pad=''; } + var tab = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; + var output = ""; + var len = input.length; + for(var i = 0; i < len; i += 3) + { + var triplet = (input.charCodeAt(i) << 16) + | (i + 1 < len ? input.charCodeAt(i+1) << 8 : 0) + | (i + 2 < len ? input.charCodeAt(i+2) : 0); + for(var j = 0; j < 4; j++) + { + if(i * 8 + j * 6 > input.length * 8) output += b64pad; + else output += tab.charAt((triplet >>> 6*(3-j)) & 0x3F); + } + } + return output; + } + + /* + * Convert a raw string to an arbitrary string encoding + */ + function rstr2any(input, encoding) + { + var divisor = encoding.length; + var remainders = Array(); + var i, q, x, quotient; + + /* Convert to an array of 16-bit big-endian values, forming the dividend */ + var dividend = Array(Math.ceil(input.length / 2)); + for(i = 0; i < dividend.length; i++) + { + dividend[i] = (input.charCodeAt(i * 2) << 8) | input.charCodeAt(i * 2 + 1); + } + + /* + * Repeatedly perform a long division. The binary array forms the dividend, + * the length of the encoding is the divisor. Once computed, the quotient + * forms the dividend for the next step. We stop when the dividend is zero. + * All remainders are stored for later use. + */ + while(dividend.length > 0) + { + quotient = Array(); + x = 0; + for(i = 0; i < dividend.length; i++) + { + x = (x << 16) + dividend[i]; + q = Math.floor(x / divisor); + x -= q * divisor; + if(quotient.length > 0 || q > 0) + quotient[quotient.length] = q; + } + remainders[remainders.length] = x; + dividend = quotient; + } + + /* Convert the remainders to the output string */ + var output = ""; + for(i = remainders.length - 1; i >= 0; i--) + output += encoding.charAt(remainders[i]); + + /* Append leading zero equivalents */ + var full_length = Math.ceil(input.length * 8 / + (Math.log(encoding.length) / Math.log(2))) + for(i = output.length; i < full_length; i++) + output = encoding[0] + output; + + return output; + } + + /* + * Encode a string as utf-8. + * For efficiency, this assumes the input is valid utf-16. + */ + function str2rstr_utf8(input) + { + var output = ""; + var i = -1; + var x, y; + + while(++i < input.length) + { + /* Decode utf-16 surrogate pairs */ + x = input.charCodeAt(i); + y = i + 1 < input.length ? input.charCodeAt(i + 1) : 0; + if(0xD800 <= x && x <= 0xDBFF && 0xDC00 <= y && y <= 0xDFFF) + { + x = 0x10000 + ((x & 0x03FF) << 10) + (y & 0x03FF); + i++; + } + + /* Encode output as utf-8 */ + if(x <= 0x7F) + output += String.fromCharCode(x); + else if(x <= 0x7FF) + output += String.fromCharCode(0xC0 | ((x >>> 6 ) & 0x1F), + 0x80 | ( x & 0x3F)); + else if(x <= 0xFFFF) + output += String.fromCharCode(0xE0 | ((x >>> 12) & 0x0F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + else if(x <= 0x1FFFFF) + output += String.fromCharCode(0xF0 | ((x >>> 18) & 0x07), + 0x80 | ((x >>> 12) & 0x3F), + 0x80 | ((x >>> 6 ) & 0x3F), + 0x80 | ( x & 0x3F)); + } + return output; + } + + /* + * Encode a string as utf-16 + */ + function str2rstr_utf16le(input) + { + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode( input.charCodeAt(i) & 0xFF, + (input.charCodeAt(i) >>> 8) & 0xFF); + return output; + } + + function str2rstr_utf16be(input) + { + var output = ""; + for(var i = 0; i < input.length; i++) + output += String.fromCharCode((input.charCodeAt(i) >>> 8) & 0xFF, + input.charCodeAt(i) & 0xFF); + return output; + } + + /* + * Convert a raw string to an array of big-endian words + * Characters >255 have their high-byte silently ignored. + */ + function rstr2binb(input) + { + var output = Array(input.length >> 2); + for(var i = 0; i < output.length; i++) + output[i] = 0; + for(var i = 0; i < input.length * 8; i += 8) + output[i>>5] |= (input.charCodeAt(i / 8) & 0xFF) << (24 - i % 32); + return output; + } + + /* + * Convert an array of big-endian words to a string + */ + function binb2rstr(input) + { + var output = ""; + for(var i = 0; i < input.length * 32; i += 8) + output += String.fromCharCode((input[i>>5] >>> (24 - i % 32)) & 0xFF); + return output; + } + + /* + * Calculate the SHA-1 of an array of big-endian words, and a bit length + */ + function binb_sha1(x, len) + { + /* append padding */ + x[len >> 5] |= 0x80 << (24 - len % 32); + x[((len + 64 >> 9) << 4) + 15] = len; + + var w = Array(80); + var a = 1732584193; + var b = -271733879; + var c = -1732584194; + var d = 271733878; + var e = -1009589776; + + for(var i = 0; i < x.length; i += 16) + { + var olda = a; + var oldb = b; + var oldc = c; + var oldd = d; + var olde = e; + + for(var j = 0; j < 80; j++) + { + if(j < 16) w[j] = x[i + j]; + else w[j] = bit_rol(w[j-3] ^ w[j-8] ^ w[j-14] ^ w[j-16], 1); + var t = safe_add(safe_add(bit_rol(a, 5), sha1_ft(j, b, c, d)), + safe_add(safe_add(e, w[j]), sha1_kt(j))); + e = d; + d = c; + c = bit_rol(b, 30); + b = a; + a = t; + } + + a = safe_add(a, olda); + b = safe_add(b, oldb); + c = safe_add(c, oldc); + d = safe_add(d, oldd); + e = safe_add(e, olde); + } + return Array(a, b, c, d, e); + + } + + /* + * Perform the appropriate triplet combination function for the current + * iteration + */ + function sha1_ft(t, b, c, d) + { + if(t < 20) return (b & c) | ((~b) & d); + if(t < 40) return b ^ c ^ d; + if(t < 60) return (b & c) | (b & d) | (c & d); + return b ^ c ^ d; + } + + /* + * Determine the appropriate additive constant for the current iteration + */ + function sha1_kt(t) + { + return (t < 20) ? 1518500249 : (t < 40) ? 1859775393 : + (t < 60) ? -1894007588 : -899497514; + } + + /* + * Add integers, wrapping at 2^32. This uses 16-bit operations internally + * to work around bugs in some JS interpreters. + */ + function safe_add(x, y) + { + var lsw = (x & 0xFFFF) + (y & 0xFFFF); + var msw = (x >> 16) + (y >> 16) + (lsw >> 16); + return (msw << 16) | (lsw & 0xFFFF); + } + + /* + * Bitwise rotate a 32-bit number to the left. + */ + function bit_rol(num, cnt) + { + return (num << cnt) | (num >>> (32 - cnt)); + } + + return { + rstr2hex: rstr2hex, binb2rstr: binb2rstr, + binb_sha1: binb_sha1, rstr2binb: rstr2binb + } +}) diff --git a/isso/js/text.js b/isso/js/text.js new file mode 100644 index 0000000..1e4fc96 --- /dev/null +++ b/isso/js/text.js @@ -0,0 +1,386 @@ +/** + * @license RequireJS text 2.0.10 Copyright (c) 2010-2012, The Dojo Foundation All Rights Reserved. + * Available via the MIT or new BSD license. + * see: http://github.com/requirejs/text for details + */ +/*jslint regexp: true */ +/*global require, XMLHttpRequest, ActiveXObject, + define, window, process, Packages, + java, location, Components, FileUtils */ + +define(['module'], function (module) { + 'use strict'; + + var text, fs, Cc, Ci, xpcIsWindows, + progIds = ['Msxml2.XMLHTTP', 'Microsoft.XMLHTTP', 'Msxml2.XMLHTTP.4.0'], + xmlRegExp = /^\s*<\?xml(\s)+version=[\'\"](\d)*.(\d)*[\'\"](\s)*\?>/im, + bodyRegExp = /]*>\s*([\s\S]+)\s*<\/body>/im, + hasLocation = typeof location !== 'undefined' && location.href, + defaultProtocol = hasLocation && location.protocol && location.protocol.replace(/\:/, ''), + defaultHostName = hasLocation && location.hostname, + defaultPort = hasLocation && (location.port || undefined), + buildMap = {}, + masterConfig = (module.config && module.config()) || {}; + + text = { + version: '2.0.10', + + strip: function (content) { + //Strips declarations so that external SVG and XML + //documents can be added to a document without worry. Also, if the string + //is an HTML document, only the part inside the body tag is returned. + if (content) { + content = content.replace(xmlRegExp, ""); + var matches = content.match(bodyRegExp); + if (matches) { + content = matches[1]; + } + } else { + content = ""; + } + return content; + }, + + jsEscape: function (content) { + return content.replace(/(['\\])/g, '\\$1') + .replace(/[\f]/g, "\\f") + .replace(/[\b]/g, "\\b") + .replace(/[\n]/g, "\\n") + .replace(/[\t]/g, "\\t") + .replace(/[\r]/g, "\\r") + .replace(/[\u2028]/g, "\\u2028") + .replace(/[\u2029]/g, "\\u2029"); + }, + + createXhr: masterConfig.createXhr || function () { + //Would love to dump the ActiveX crap in here. Need IE 6 to die first. + var xhr, i, progId; + if (typeof XMLHttpRequest !== "undefined") { + return new XMLHttpRequest(); + } else if (typeof ActiveXObject !== "undefined") { + for (i = 0; i < 3; i += 1) { + progId = progIds[i]; + try { + xhr = new ActiveXObject(progId); + } catch (e) {} + + if (xhr) { + progIds = [progId]; // so faster next time + break; + } + } + } + + return xhr; + }, + + /** + * Parses a resource name into its component parts. Resource names + * look like: module/name.ext!strip, where the !strip part is + * optional. + * @param {String} name the resource name + * @returns {Object} with properties "moduleName", "ext" and "strip" + * where strip is a boolean. + */ + parseName: function (name) { + var modName, ext, temp, + strip = false, + index = name.indexOf("."), + isRelative = name.indexOf('./') === 0 || + name.indexOf('../') === 0; + + if (index !== -1 && (!isRelative || index > 1)) { + modName = name.substring(0, index); + ext = name.substring(index + 1, name.length); + } else { + modName = name; + } + + temp = ext || modName; + index = temp.indexOf("!"); + if (index !== -1) { + //Pull off the strip arg. + strip = temp.substring(index + 1) === "strip"; + temp = temp.substring(0, index); + if (ext) { + ext = temp; + } else { + modName = temp; + } + } + + return { + moduleName: modName, + ext: ext, + strip: strip + }; + }, + + xdRegExp: /^((\w+)\:)?\/\/([^\/\\]+)/, + + /** + * Is an URL on another domain. Only works for browser use, returns + * false in non-browser environments. Only used to know if an + * optimized .js version of a text resource should be loaded + * instead. + * @param {String} url + * @returns Boolean + */ + useXhr: function (url, protocol, hostname, port) { + var uProtocol, uHostName, uPort, + match = text.xdRegExp.exec(url); + if (!match) { + return true; + } + uProtocol = match[2]; + uHostName = match[3]; + + uHostName = uHostName.split(':'); + uPort = uHostName[1]; + uHostName = uHostName[0]; + + return (!uProtocol || uProtocol === protocol) && + (!uHostName || uHostName.toLowerCase() === hostname.toLowerCase()) && + ((!uPort && !uHostName) || uPort === port); + }, + + finishLoad: function (name, strip, content, onLoad) { + content = strip ? text.strip(content) : content; + if (masterConfig.isBuild) { + buildMap[name] = content; + } + onLoad(content); + }, + + load: function (name, req, onLoad, config) { + //Name has format: some.module.filext!strip + //The strip part is optional. + //if strip is present, then that means only get the string contents + //inside a body tag in an HTML string. For XML/SVG content it means + //removing the declarations so the content can be inserted + //into the current doc without problems. + + // Do not bother with the work if a build and text will + // not be inlined. + if (config.isBuild && !config.inlineText) { + onLoad(); + return; + } + + masterConfig.isBuild = config.isBuild; + + var parsed = text.parseName(name), + nonStripName = parsed.moduleName + + (parsed.ext ? '.' + parsed.ext : ''), + url = req.toUrl(nonStripName), + useXhr = (masterConfig.useXhr) || + text.useXhr; + + // Do not load if it is an empty: url + if (url.indexOf('empty:') === 0) { + onLoad(); + return; + } + + //Load the text. Use XHR if possible and in a browser. + if (!hasLocation || useXhr(url, defaultProtocol, defaultHostName, defaultPort)) { + text.get(url, function (content) { + text.finishLoad(name, parsed.strip, content, onLoad); + }, function (err) { + if (onLoad.error) { + onLoad.error(err); + } + }); + } else { + //Need to fetch the resource across domains. Assume + //the resource has been optimized into a JS module. Fetch + //by the module name + extension, but do not include the + //!strip part to avoid file system issues. + req([nonStripName], function (content) { + text.finishLoad(parsed.moduleName + '.' + parsed.ext, + parsed.strip, content, onLoad); + }); + } + }, + + write: function (pluginName, moduleName, write, config) { + if (buildMap.hasOwnProperty(moduleName)) { + var content = text.jsEscape(buildMap[moduleName]); + write.asModule(pluginName + "!" + moduleName, + "define(function () { return '" + + content + + "';});\n"); + } + }, + + writeFile: function (pluginName, moduleName, req, write, config) { + var parsed = text.parseName(moduleName), + extPart = parsed.ext ? '.' + parsed.ext : '', + nonStripName = parsed.moduleName + extPart, + //Use a '.js' file name so that it indicates it is a + //script that can be loaded across domains. + fileName = req.toUrl(parsed.moduleName + extPart) + '.js'; + + //Leverage own load() method to load plugin value, but only + //write out values that do not have the strip argument, + //to avoid any potential issues with ! in file names. + text.load(nonStripName, req, function (value) { + //Use own write() method to construct full module value. + //But need to create shell that translates writeFile's + //write() to the right interface. + var textWrite = function (contents) { + return write(fileName, contents); + }; + textWrite.asModule = function (moduleName, contents) { + return write.asModule(moduleName, fileName, contents); + }; + + text.write(pluginName, nonStripName, textWrite, config); + }, config); + } + }; + + if (masterConfig.env === 'node' || (!masterConfig.env && + typeof process !== "undefined" && + process.versions && + !!process.versions.node && + !process.versions['node-webkit'])) { + //Using special require.nodeRequire, something added by r.js. + fs = require.nodeRequire('fs'); + + text.get = function (url, callback, errback) { + try { + var file = fs.readFileSync(url, 'utf8'); + //Remove BOM (Byte Mark Order) from utf8 files if it is there. + if (file.indexOf('\uFEFF') === 0) { + file = file.substring(1); + } + callback(file); + } catch (e) { + errback(e); + } + }; + } else if (masterConfig.env === 'xhr' || (!masterConfig.env && + text.createXhr())) { + text.get = function (url, callback, errback, headers) { + var xhr = text.createXhr(), header; + xhr.open('GET', url, true); + + //Allow plugins direct access to xhr headers + if (headers) { + for (header in headers) { + if (headers.hasOwnProperty(header)) { + xhr.setRequestHeader(header.toLowerCase(), headers[header]); + } + } + } + + //Allow overrides specified in config + if (masterConfig.onXhr) { + masterConfig.onXhr(xhr, url); + } + + xhr.onreadystatechange = function (evt) { + var status, err; + //Do not explicitly handle errors, those should be + //visible via console output in the browser. + if (xhr.readyState === 4) { + status = xhr.status; + if (status > 399 && status < 600) { + //An http 4xx or 5xx error. Signal an error. + err = new Error(url + ' HTTP status: ' + status); + err.xhr = xhr; + errback(err); + } else { + callback(xhr.responseText); + } + + if (masterConfig.onXhrComplete) { + masterConfig.onXhrComplete(xhr, url); + } + } + }; + xhr.send(null); + }; + } else if (masterConfig.env === 'rhino' || (!masterConfig.env && + typeof Packages !== 'undefined' && typeof java !== 'undefined')) { + //Why Java, why is this so awkward? + text.get = function (url, callback) { + var stringBuffer, line, + encoding = "utf-8", + file = new java.io.File(url), + lineSeparator = java.lang.System.getProperty("line.separator"), + input = new java.io.BufferedReader(new java.io.InputStreamReader(new java.io.FileInputStream(file), encoding)), + content = ''; + try { + stringBuffer = new java.lang.StringBuffer(); + line = input.readLine(); + + // Byte Order Mark (BOM) - The Unicode Standard, version 3.0, page 324 + // http://www.unicode.org/faq/utf_bom.html + + // Note that when we use utf-8, the BOM should appear as "EF BB BF", but it doesn't due to this bug in the JDK: + // http://bugs.sun.com/bugdatabase/view_bug.do?bug_id=4508058 + if (line && line.length() && line.charAt(0) === 0xfeff) { + // Eat the BOM, since we've already found the encoding on this file, + // and we plan to concatenating this buffer with others; the BOM should + // only appear at the top of a file. + line = line.substring(1); + } + + if (line !== null) { + stringBuffer.append(line); + } + + while ((line = input.readLine()) !== null) { + stringBuffer.append(lineSeparator); + stringBuffer.append(line); + } + //Make sure we return a JavaScript string and not a Java string. + content = String(stringBuffer.toString()); //String + } finally { + input.close(); + } + callback(content); + }; + } else if (masterConfig.env === 'xpconnect' || (!masterConfig.env && + typeof Components !== 'undefined' && Components.classes && + Components.interfaces)) { + //Avert your gaze! + Cc = Components.classes, + Ci = Components.interfaces; + Components.utils['import']('resource://gre/modules/FileUtils.jsm'); + xpcIsWindows = ('@mozilla.org/windows-registry-key;1' in Cc); + + text.get = function (url, callback) { + var inStream, convertStream, fileObj, + readData = {}; + + if (xpcIsWindows) { + url = url.replace(/\//g, '\\'); + } + + fileObj = new FileUtils.File(url); + + //XPCOM, you so crazy + try { + inStream = Cc['@mozilla.org/network/file-input-stream;1'] + .createInstance(Ci.nsIFileInputStream); + inStream.init(fileObj, 1, 0, false); + + convertStream = Cc['@mozilla.org/intl/converter-input-stream;1'] + .createInstance(Ci.nsIConverterInputStream); + convertStream.init(inStream, "utf-8", inStream.available(), + Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER); + + convertStream.readString(inStream.available(), readData); + convertStream.close(); + inStream.close(); + callback(readData.value); + } catch (e) { + throw new Error((fileObj && fileObj.path || '') + ': ' + e); + } + }; + } + return text; +}); diff --git a/isso/js/text/arrow-down.svg b/isso/js/text/arrow-down.svg new file mode 100755 index 0000000..d6cd449 --- /dev/null +++ b/isso/js/text/arrow-down.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/isso/js/text/arrow-up.svg b/isso/js/text/arrow-up.svg new file mode 100755 index 0000000..cebd089 --- /dev/null +++ b/isso/js/text/arrow-up.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/isso/js/text/comment.html b/isso/js/text/comment.html new file mode 100644 index 0000000..f440663 --- /dev/null +++ b/isso/js/text/comment.html @@ -0,0 +1,61 @@ +
+
+ +
+
+
+ {{ if bool(website) }} + + {{ author | blank : Anonymous }} + + {{ else }} + + {{ author | blank : Anonymous }} + + {{ /if }} + + {{ if parent }} + + {{ svg-forward }}{{ replyto }} + {{ /if }} + + + + + + + {{ if mode | equals : 2 }} + Kommentar muss noch freigeschaltet werden. + {{ /if }} + {{ if mode | equals : 4 }} + Kommentar gelöscht. + {{ /if }} + + + +
+
+ {{ if mode | equals : 4 }} +

 

+ {{ else }} + {{ text }} + {{ /if }} +
+ +
+
+
+
\ No newline at end of file diff --git a/isso/js/text/forward.svg b/isso/js/text/forward.svg new file mode 100755 index 0000000..e4ddb8f --- /dev/null +++ b/isso/js/text/forward.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/isso/js/text/html.js b/isso/js/text/html.js new file mode 100644 index 0000000..d50a479 --- /dev/null +++ b/isso/js/text/html.js @@ -0,0 +1,6 @@ +define(["text!./postbox.html", "text!./comment.html"], function (postbox, comment) { + return { + postbox: postbox, + comment: comment + }; +}); \ No newline at end of file diff --git a/isso/js/text/postbox.html b/isso/js/text/postbox.html new file mode 100644 index 0000000..e1d031a --- /dev/null +++ b/isso/js/text/postbox.html @@ -0,0 +1,21 @@ +
+
+ +
+
+
+ +
+
+

+ +

+

+ +

+

+ +

+
+
+
\ No newline at end of file diff --git a/isso/js/text/svg.js b/isso/js/text/svg.js new file mode 100644 index 0000000..dce1319 --- /dev/null +++ b/isso/js/text/svg.js @@ -0,0 +1,7 @@ +define(["text!./forward.svg", "text!./arrow-down.svg", "text!./arrow-up.svg"], function (forward, arrdown, arrup) { + return { + "forward": forward, + "arrow-down": arrdown, + "arrow-up": arrup + }; +}); \ No newline at end of file diff --git a/isso/static/isso.css b/isso/static/isso.css index 57db242..8f918d6 100644 --- a/isso/static/isso.css +++ b/isso/static/isso.css @@ -31,6 +31,10 @@ text-shadow: #aaaaaa 0px 0px 1px; } +span.votes, a.like, a.dislike { + color: gray; +} + .isso-comment > header, .isso-comment > footer { font-family: "Helvetica", Arial, sans-serif; font-size: 0.80em; diff --git a/isso/static/post.html b/isso/static/post.html index 0bbd738..ebca347 100644 --- a/isso/static/post.html +++ b/isso/static/post.html @@ -3,8 +3,10 @@ Hello World - - + + + +