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;
iframe.innerHTML = 'Cannot show iframes.';
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 (document.body.contains(el) && (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\.|gaming\.)?(?: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 {
let pp = {}; args.replace(/^\?/, '')
.split('&')
.map(s => s.split('='))
.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;
let argsStr = Object.keys(embedArgs)
.map(k => `${k}=${embedArgs[k]}`)
.join('&');
iframeUrl = `//www.youtube-nocookie.com/embed/${v}?${argsStr}`;
}
let iframe = makeIframe(iframeUrl, '100%', '360px');
iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0);
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');
iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0);
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.querySelector('#content article[data-mid]')) {
embedLinksToArticles();
} else {
embedLinksToPost();
}
}
exports.embedAll = embedAll;
exports.embedLinksToX = embedLinksToX;
exports.format = juickFormat;