function insertAfter(newNode, referenceNode) { referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling); } function htmlEscape(html) { let textarea = document.createElement('textarea'); textarea.textContent = html; return textarea.innerHTML; } // 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 subsequentRules = rules.filter(r => r.pr >= rule.pr); 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]), subsequentRules) + r; })() : match[0].replace(rule.re, rule.with); return ft(outerStr, subsequentRules).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') { let iframe = document.createElement('iframe'); iframe.style.width = w; iframe.style.height = h; iframe.frameBorder = 0; iframe.scrolling = scrolling; iframe.setAttribute('allowFullScreen', ''); iframe.src = src; return iframe; } function wrapIntoTag(node, tagName, className) { let tag = document.createElement(tagName); if (className !== undefined) { tag.className = className; } tag.appendChild(node); return tag; } function makeResizableToRatio(element, ratio) { element.dataset['ratio'] = ratio; makeResizable(element, w => w * element.dataset['ratio']); } // calcHeight :: Number -> Number -- calculate element height for a given width function makeResizable(element, calcHeight) { const setHeight = el => { if (el.offsetWidth > 0) { el.style.height = (calcHeight(el.offsetWidth)).toFixed(2) + 'px'; } }; window.addEventListener('resize', () => setHeight(element)); 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 [ { name: 'Jpeg and png images', id: 'embed_jpeg_and_png_images', ctsDefault: false, re: /\.(jpeg|jpg|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i, makeNode: function(aNode) { let aNode2 = document.createElement('a'); let imgNode = document.createElement('img'); imgNode.src = aNode.href; aNode2.href = aNode.href; aNode2.appendChild(imgNode); return wrapIntoTag(aNode2, 'div', 'picture compact'); } }, { name: 'Gif images', id: 'embed_gif_images', ctsDefault: true, re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i, makeNode: function(aNode) { let aNode2 = document.createElement('a'); let imgNode = document.createElement('img'); imgNode.src = aNode.href; aNode2.href = aNode.href; aNode2.appendChild(imgNode); return wrapIntoTag(aNode2, 'div', 'picture compact'); } }, { name: 'Video (webm, mp4, ogv)', id: 'embed_webm_and_mp4_videos', ctsDefault: false, re: /\.(webm|mp4|m4v|ogv)(?:\?[\w&;\?=]*)?$/i, makeNode: function(aNode) { let video = document.createElement('video'); video.src = aNode.href; video.title = aNode.href; video.setAttribute('controls', ''); return wrapIntoTag(video, 'div', 'video compact'); } }, { name: 'Audio (mp3, ogg, weba, opus, m4a, oga, wav)', id: 'embed_sound_files', ctsDefault: false, re: /\.(mp3|ogg|weba|opus|m4a|oga|wav)(?:\?[\w&;\?=]*)?$/i, makeNode: function(aNode) { let audio = document.createElement('audio'); audio.src = aNode.href; audio.title = aNode.href; audio.setAttribute('controls', ''); return wrapIntoTag(audio, 'div', 'audio'); } }, { name: 'YouTube videos (and playlists)', id: 'embed_youtube_videos', onByDefault: false, ctsDefault: false, re: /^(?:https?:)?\/\/(?:www\.|m\.)?(?:youtu(?:(?:\.be\/|be\.com\/(?:v|embed)\/)([-\w]+)|be\.com\/watch)((?:(?:\?|&(?:amp;)?)(?:\w+=[-\.\w]*[-\w]))*)|youtube\.com\/playlist\?list=([-\w]*)(&(amp;)?[-\w\?=]*)?)/i, makeNode: function(aNode, reResult) { let [url, v, args, plist] = reResult; let iframeUrl; if (plist !== undefined) { iframeUrl = '//www.youtube-nocookie.com/embed/videoseries?list=' + plist; } else { args = args.replace(/^\?/, ''); let arr = args.split('&').map(s => s.split('=')); let pp = {}; arr.forEach(z => pp[z[0]] = z[1]); let embedArgs = { rel: '0' }; if (pp.t != undefined) { const tre = /^(?:(\d+)|(?:(\d+)h)?(?:(\d+)m)?(\d+)s|(?:(\d+)h)?(\d+)m|(\d+)h)$/i; let [, t, h, m, s, h1, m1, h2] = tre.exec(pp.t); embedArgs['start'] = (+t) || ((+(h || h1 || h2 || 0))*60*60 + (+(m || m1 || 0))*60 + (+(s || 0))); } if (pp.list !== undefined) { embedArgs['list'] = pp.list; } v = v || pp.v; iframeUrl = '//www.youtube-nocookie.com/embed/' + v + '?' + Object.keys(embedArgs).map(k => `${k}=${embedArgs[k]}`).join('&'); } let iframe = makeIframe(iframeUrl, '100%', '360px'); setTimeout(() => makeResizableToRatio(iframe, 9.0/16.0), 10); return wrapIntoTag(iframe, 'div', 'youtube resizableV'); } }, { name: 'Vimeo videos', id: 'embed_vimeo_videos', onByDefault: false, ctsDefault: false, //re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:(?:video\/|album\/[\d]+\/video\/)?([\d]+)|([\w-]+)\/(?!videos)([\w-]+))/i, re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/|album\/[\d]+\/video\/)?([\d]+)/i, makeNode: function(aNode, reResult) { let iframe = makeIframe('//player.vimeo.com/video/' + reResult[1], '100%', '360px'); setTimeout(() => makeResizableToRatio(iframe, 9.0/16.0), 10); return wrapIntoTag(iframe, 'div', 'vimeo resizableV'); } } ]; } function embedLink(aNode, linkTypes, container, afterNode) { let anyEmbed = false; let linkId = (aNode.href.replace(/^https?:/i, '').replace(/\'/i,'')); let sameEmbed = container.querySelector(`*[data-linkid='${linkId}']`); // do not embed the same thing twice if (sameEmbed === null) { anyEmbed = [].some.call(linkTypes, function(linkType) { let reResult = linkType.re.exec(aNode.href); if (reResult !== null) { if ((linkType.match !== undefined) && (linkType.match(aNode, reResult) === false)) { return false; } let newNode = linkType.makeNode(aNode, reResult); if (!newNode) { return false; } newNode.setAttribute('data-linkid', linkId); if (afterNode !== undefined) { insertAfter(newNode, afterNode); } else { container.appendChild(newNode); } aNode.classList.add('embedLink'); return true; } }); } return anyEmbed; } function embedLinks(aNodes, container) { let anyEmbed = false; let embeddableLinkTypes = getEmbeddableLinkTypes(); Array.from(aNodes).forEach(aNode => { let isEmbedded = embedLink(aNode, embeddableLinkTypes, container); anyEmbed = anyEmbed || isEmbedded; }); 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); let existingContainer = x.querySelector('div.embedContainer'); if (existingContainer !== null) { embedLinks(allLinks, existingContainer); } else { let embedContainer = document.createElement('div'); embedContainer.className = 'embedContainer'; let anyEmbed = embedLinks(allLinks, embedContainer); if (anyEmbed) { let beforeNode = x.querySelector(beforeNodeSelector); x.insertBefore(embedContainer, beforeNode); } } } function embedLinksToArticles() { let beforeNodeSelector = 'nav.l'; let allLinksSelector = 'p:not(.ir) a, pre a'; Array.from(document.querySelectorAll('#content > article')).forEach(article => { embedLinksToX(article, beforeNodeSelector, allLinksSelector); }); } function embedLinksToPost() { let beforeNodeSelector = '.msg-txt + *'; let allLinksSelector = '.msg-txt a'; Array.from(document.querySelectorAll('#content .msg-cont')).forEach(msg => { embedLinksToX(msg, beforeNodeSelector, allLinksSelector); }); } /** * 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;