From 894886e808cb8957e83c05921d2452421f1defd4 Mon Sep 17 00:00:00 2001 From: KillyMXI Date: Sat, 17 Jun 2017 20:46:19 +0300 Subject: juick-www: incoming reply format and photo --- juick-www/src/main/js/killy/index.js | 112 +++++++++++++++++++++++++++++++++-- juick-www/src/main/static/scripts.js | 97 +++++++++++------------------- 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) + ? `${p1}` + : `${extractDomain(match)}`; +} + +function urlReplaceInCode(match, p1, p2, p3) { + let isBrackets = (p1 !== undefined); + return (isBrackets) + ? `${match}` + : `${match}`; +} + +function messageReplyReplace(messageId) { + return function(match, mid, rid) { + let replyPart = (rid && rid != '0') ? '#' + rid : ''; + return `${match}`; + }; +} + +/** + * 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(/^(?:>|>)\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: '@$1' }, + ]) + : formatText(txt, [ + { pr: 0, re: /((?:^(?:>|>)\s?[\s\S]+?$\n?)+)/gmi, brackets: true, with: ['', '', 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: '@$1' }, + { pr: 2, re: /\B\*([^\*\n<>]+)\*((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] }, + { pr: 2, re: /\B\/([^\/\n<>]+)\/((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] }, + { pr: 2, re: /\b\_([^\_\n<>]+)\_((?=\s)|(?=$)|(?=[!\"#$%&'*+,\-./:;<=>?@[\]^_`{|}~()]+))/g, brackets: true, with: ['', ''] }, + { pr: 3, re: /\n/g, with: '
' }, + ]); +} + 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 += ` в ответ на /${msg.replyto}`; + } + let photoDiv = (msg.photo === undefined) ? '' : ` +
+ +
`; + let msgContHtml = ` +
+
+ @${msg.user.uname}: +
+ ${msg.user.uname} +
+
+ +
+
${killy.format(msg.body, msg.mid, false)}
${photoDiv} + + +
`; + + 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 += ' в ответ на /' + msg.replyto + ''; - } - msgLinks.innerHTML = msgNum + ' · '; - 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(); }); -- cgit v1.2.3