diff options
-rw-r--r-- | juick-www/src/main/js/killy/index.js | 112 | ||||
-rw-r--r-- | 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) + ? `<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(/^(?:>|>)\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: /((?:^(?:>|>)\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} · <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 + ' · '; - 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(); }); |