aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--juick-www/src/main/js/killy/index.js112
-rw-r--r--juick-www/src/main/static/scripts.js97
2 files changed, 141 insertions, 68 deletions
diff --git a/juick-www/src/main/js/killy/index.js b/juick-www/src/main/js/killy/index.js
index c2ce20fb..634dfc42 100644
--- a/juick-www/src/main/js/killy/index.js
+++ b/juick-www/src/main/js/killy/index.js
@@ -3,12 +3,39 @@ function insertAfter(newNode, referenceNode) {
referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
}
-function moveAll(fromNode, toNode) {
- for (let c; c = fromNode.firstChild; ) { toNode.appendChild(c); }
+function htmlEscape(html) {
+ let textarea = document.createElement('textarea');
+ textarea.textContent = html;
+ return textarea.innerHTML;
}
-function removeAllFrom(fromNode) {
- for (let c; c = fromNode.lastChild; ) { fromNode.removeChild(c); }
+// rules :: [{pr: number, re: RegExp, with: string}]
+// rules :: [{pr: number, re: RegExp, with: Function}]
+// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string]}]
+// rules :: [{pr: number, re: RegExp, brackets: true, with: [string, string, Function]}]
+function formatText(txt, rules) {
+ let idCounter = 0;
+ function nextId() { return idCounter++; }
+ function ft(txt, rules) {
+ let matches = rules.map(r => { r.re.lastIndex = 0; return [r, r.re.exec(txt)]; })
+ .filter(([,m]) => m !== null)
+ .sort(([r1,m1],[r2,m2]) => (r1.pr - r2.pr) || (m1.index - m2.index));
+ if (matches && matches.length > 0) {
+ let [rule, match] = matches[0];
+ let idStr = `<>(${nextId()})<>`;
+ let outerStr = txt.substring(0, match.index) + idStr + txt.substring(rule.re.lastIndex);
+ let innerStr = (rule.brackets)
+ ? (() => { let [l ,r ,f] = rule.with; return l + ft((f ? f(match[1]) : match[1]), rules) + r; })()
+ : match[0].replace(rule.re, rule.with);
+ return ft(outerStr, rules).replace(idStr, innerStr);
+ }
+ return txt;
+ }
+ return ft(htmlEscape(txt), rules); // idStr above relies on the fact the text is escaped
+}
+
+function fixWwwLink(url) {
+ return url.replace(/^(?!([a-z]+:)?\/\/)/i, '//');
}
function makeIframe(src, w, h, scrolling='no') {
@@ -45,6 +72,62 @@ function makeResizable(element, calcHeight) {
setHeight(element);
}
+function extractDomain(url) {
+ const domainRe = /^(?:https?:\/\/)?(?:[^@\/\n]+@)?(?:www\.)?([^:\/\n]+)/i;
+ return domainRe.exec(url)[1];
+}
+
+function urlReplace(match, p1, p2, p3) {
+ let isBrackets = (p1 !== undefined);
+ return (isBrackets)
+ ? `<a href="${fixWwwLink(p2 || p3)}">${p1}</a>`
+ : `<a href="${fixWwwLink(match)}">${extractDomain(match)}</a>`;
+}
+
+function urlReplaceInCode(match, p1, p2, p3) {
+ let isBrackets = (p1 !== undefined);
+ return (isBrackets)
+ ? `<a href="${fixWwwLink(p2 || p3)}">${match}</a>`
+ : `<a href="${fixWwwLink(match)}">${match}</a>`;
+}
+
+function messageReplyReplace(messageId) {
+ return function(match, mid, rid) {
+ let replyPart = (rid && rid != '0') ? '#' + rid : '';
+ return `<a href="//${window.location.hostname}/${mid || messageId}${replyPart}">${match}</a>`;
+ };
+}
+
+/**
+ * Given "txt" message in unescaped plaintext with Juick markup, this function
+ * returns escaped formatted HTML string.
+ *
+ * @param {string} txt
+ * @param {string} messageId - current message id
+ * @param {boolean} isCode
+ * @returns {string}
+ */
+function juickFormat(txt, messageId, isCode) {
+ const urlRe = /(?:\[([^\]\[]+)\](?:\[([^\]]+)\]|\(((?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))\))|\b(?:[a-z]+:\/\/|www\.|ftp\.)(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[-\w+*&@#/%=~|$?!:;,.])*(?:\([-\w+*&@#/%=~|$?!:;,.]*\)|[\w+*&@#/%=~|$]))/gi;
+ const bqReplace = m => m.replace(/^(?:>|&gt;)\s?/gmi, '');
+ return (isCode)
+ ? formatText(txt, [
+ { pr: 1, re: urlRe, with: urlReplaceInCode },
+ { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) },
+ { pr: 1, re: /\B@([\w-]+)\b/gi, with: '<a href="//' + window.location.hostname + '/$1">@$1</a>' },
+ ])
+ : formatText(txt, [
+ { pr: 0, re: /((?:^(?:>|&gt;)\s?[\s\S]+?$\n?)+)/gmi, brackets: true, with: ['<q>', '</q>', bqReplace] },
+ { pr: 1, re: urlRe, with: urlReplace },
+ { pr: 1, re: /\B(?:#(\d+))?(?:\/(\d+))?\b/g, with: messageReplyReplace(messageId) },
+ { pr: 1, re: /\B@([\w-]+)\b/gi, with: '<a href="//' + window.location.hostname + '/$1">@$1</a>' },
+ { pr: 2, re: /\B\*([^\*\n<>]+)\*((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<b>', '</b>'] },
+ { pr: 2, re: /\B\/([^\/\n<>]+)\/((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<i>', '</i>'] },
+ { pr: 2, re: /\b\_([^\_\n<>]+)\_((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['<span class="u">', '</span>'] },
+ { pr: 3, re: /\n/g, with: '<br/>' },
+ ]);
+}
+
function getEmbeddableLinkTypes() {
return [
{
@@ -184,6 +267,16 @@ function embedLinks(aNodes, container) {
return anyEmbed;
}
+/**
+ * Embed all the links inside element "x" that match to "allLinksSelector".
+ * All the embedded media is placed inside "div.embedContainer".
+ * "div.embedContainer" is inserted before an element matched by "beforeNodeSelector"
+ * if not present. Existing container is used otherwise.
+ *
+ * @param {Element} x
+ * @param {string} beforeNodeSelector
+ * @param {string} allLinksSelector
+ */
function embedLinksToX(x, beforeNodeSelector, allLinksSelector) {
let isCtsPost = false;
let allLinks = x.querySelectorAll(allLinksSelector);
@@ -219,10 +312,17 @@ function embedLinksToPost() {
});
}
-exports.embed = function() {
+/**
+ * Embed all the links in all messages/replies on the page.
+ */
+function embedAll() {
if (document.querySelectorAll('#content > article').length) {
embedLinksToArticles();
} else {
embedLinksToPost();
}
-};
+}
+
+exports.embedAll = embedAll;
+exports.embedLinksToX = embedLinksToX;
+exports.format = juickFormat;
diff --git a/juick-www/src/main/static/scripts.js b/juick-www/src/main/static/scripts.js
index e3d2445a..22731671 100644
--- a/juick-www/src/main/static/scripts.js
+++ b/juick-www/src/main/static/scripts.js
@@ -85,79 +85,52 @@ function isTreeMode() {
}
function wsIncomingReply(msg) {
- var li = document.createElement('li');
+ const unsetProto = (url) => url.replace(/^(https?:)?(?=\/\/)/i, '');
+ let msgNum = '/' + msg.rid;
+ if (msg.replyto > 0) {
+ msgNum += ` в ответ на <a href="#${msg.replyto}">/${msg.replyto}</a>`;
+ }
+ let photoDiv = (msg.photo === undefined) ? '' : `
+ <div class="msg-media"><a href="//i.juick.com/p/${msg.mid}.${msg.attach}">
+ <img src="${unsetProto(msg.photo.small)}"/></a>
+ </div>`;
+ let msgContHtml = `
+ <div class="msg-cont">
+ <div class="msg-header">
+ @<a href="/${msg.user.uname}/">${msg.user.uname}</a>:
+ <div class="msg-avatar">
+ <a href="/${msg.user.uname}/"><img src="//i.juick.com/a/${msg.user.uid}.png" alt="${msg.user.uname}"/></a>
+ </div>
+ <div class="msg-menu"><a href="#" class="a-thread-links"></a></div>
+ <div class="msg-ts">
+ <a href="/${msg.mid}#${msg.rid}" title="${msg.timestamp}GMT">${msg.timestamp}</a>
+ </div>
+ </div>
+ <div class="msg-txt">${killy.format(msg.body, msg.mid, false)}</div>${photoDiv}
+ <div class="msg-links">${msgNum} &middot; <a class="msg-reply-link" href="#">Ответить</a></div>
+ <div class="msg-comment" style="display: none;"></div>
+ </div>`;
+
+ let li = document.createElement('li');
li.setAttribute('class', 'msg reply-new');
li.setAttribute('id', msg.rid);
+ li.innerHTML = msgContHtml;
li.addEventListener('click', newReply);
li.addEventListener('mouseover', newReply);
- var msgAvatar = document.createElement('div');
- msgAvatar.setAttribute('class', 'msg-avatar');
- var msgAvatarLink = document.createElement('a');
- msgAvatarLink.setAttribute('href', '/' + msg.user.uname + '/');
- var msgAvatarImg = document.createElement('img');
- msgAvatarImg.setAttribute('src', '//i.juick.com/a/' + msg.user.uid + '.png');
- msgAvatarLink.appendChild(msgAvatarImg);
- msgAvatar.appendChild(msgAvatarLink);
-
- var msgCont = document.createElement('div');
- msgCont.setAttribute('class', 'msg-cont');
- var msgMenu = document.createElement('div');
- msgMenu.setAttribute('class', 'msg-menu');
- msgCont.appendChild(msgMenu);
- var msgMenuLink = document.createElement('a');
- msgMenuLink.setAttribute('href', '#');
- msgMenuLink.addEventListener('click', function (e) {
+ li.querySelector('.msg-menu > a').addEventListener('click', function (e) {
showMessageLinksDialog(msg.mid, msg.rid);
e.preventDefault();
});
- msgMenu.appendChild(msgMenuLink);
- var msgHeader = document.createElement('div');
- msgHeader.setAttribute('class', 'msg-header');
- var msgHeaderLink = document.createElement('a');
- msgHeaderLink.setAttribute('href', '/' + msg.user.uname + '/');
- msgHeaderLink.textContent = '@' + msg.user.uname + ':';
- msgHeader.appendChild(msgHeaderLink);
- var msgTimestamp = document.createElement('div');
- msgTimestamp.setAttribute('class', 'msg-ts');
- var msgTimestampLink = document.createElement('a');
- msgTimestampLink.setAttribute('href', '/' + msg.mid + '#' + msg.rid);
- msgTimestampLink.setAttribute('title', msg.timestamp + ' GMT');
- msgTimestampLink.textContent = msg.timestamp;
- msgTimestamp.appendChild(msgTimestampLink);
- var msgTxt = document.createElement('div');
- msgTxt.setAttribute('class', 'msg-txt');
- var msgLinks = document.createElement('div');
- msgLinks.setAttribute('class', 'msg-links');
- var msgNum = '/' + msg.rid;
- if (msg.replyto > 0) {
- msgNum += ' в ответ на <a href="#' + msg.replyto + '">/' + msg.replyto + '</a>';
- }
- msgLinks.innerHTML = msgNum + ' &middot; ';
- var msgLinksLink = document.createElement('a');
- msgLinksLink.setAttribute('href', '#');
- msgLinksLink.textContent = 'Ответить';
- msgLinksLink.addEventListener('click', function (e) {
+ li.querySelector('a.msg-reply-link').addEventListener('click', function (e) {
showCommentForm(msg.mid, msg.rid);
e.preventDefault();
});
- msgLinks.appendChild(msgLinksLink);
- var msgComment = document.createElement('div');
- msgComment.setAttribute('class', 'msg-comment');
- msgComment.style.display = 'none';
- msgHeader.appendChild(msgAvatar);
- msgHeader.appendChild(msgMenu);
- msgHeader.appendChild(msgTimestamp);
- msgCont.appendChild(msgHeader);
- msgCont.appendChild(msgTxt);
- msgCont.appendChild(msgLinks);
- msgCont.appendChild(msgComment);
- li.appendChild(msgCont);
-
- li.querySelector('.msg-txt').textContent = msg.body;
+
+ killy.embedLinksToX(li.querySelector('.msg-cont'), '.msg-links', '.msg-txt a');
if (isTreeMode() && (msg.replyto > 0)) {
- var p = document.getElementById(msg.replyto);
- var m = parseInt(p.style.marginLeft) + 20;
+ let p = document.getElementById(msg.replyto);
+ let m = parseInt(p.style.marginLeft) + 20;
while (p.nextElementSibling && (parseInt(p.nextElementSibling.style.marginLeft) >= m)) { p = p.nextElementSibling; }
li.style.marginLeft = m + 'px';
p.parentNode.insertBefore(li, p.nextSibling);
@@ -748,5 +721,5 @@ ready(function () {
window.addEventListener('pagehide', wsShutdown);
- killy.embed();
+ killy.embedAll();
});