aboutsummaryrefslogtreecommitdiff
path: root/src
diff options
context:
space:
mode:
authorGravatar Vitaly Takmazov2023-01-14 10:12:56 +0300
committerGravatar Vitaly Takmazov2023-01-14 10:12:56 +0300
commit116024102e93f2444f4f566bea83b5f3d98ccb25 (patch)
tree0f55652068d7bc92ecf6d1acbb663f7bf0ebf290 /src
parente3c378cbf1d502263c61d3b9c31cd270bc3ae239 (diff)
embed.js: merge production and vnext
Diffstat (limited to 'src')
-rw-r--r--src/main/assets/embed.js396
-rw-r--r--src/main/assets/scripts.js2
2 files changed, 1 insertions, 397 deletions
diff --git a/src/main/assets/embed.js b/src/main/assets/embed.js
deleted file mode 100644
index 0ef76bc2..00000000
--- a/src/main/assets/embed.js
+++ /dev/null
@@ -1,396 +0,0 @@
-function insertAfter(newNode, referenceNode) {
- referenceNode.parentNode.insertBefore(newNode, referenceNode.nextSibling);
-}
-
-function setContent(containerNode, ...newNodes) {
- removeAllFrom(containerNode);
- newNodes.forEach(n => containerNode.appendChild(n));
- return containerNode;
-}
-
-function removeAllFrom(fromNode) {
- for (let c; c = fromNode.lastChild;) { fromNode.removeChild(c); }
-}
-
-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 makeNewNode(embedType, aNode, reResult) {
- const withClasses = el => {
- if (embedType.className) {
- el.classList.add(...embedType.className.split(' '));
- }
- return el;
- };
- return embedType.makeNode(aNode, reResult, withClasses(document.createElement('div')));
-}
-
-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 makeResizableToRatio(element, ratio) {
- element.setAttribute('data-ratio', ratio);
- makeResizable(element, w => w * element.getAttribute('data-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)
- ? `<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="/${mid || messageId}${replyPart}">${match}</a>`;
- };
-}
-
-/**
- * Given "txt" message in unescaped plaintext with Juick markup, this function
- * returns escaped formatted HTML string.
- *
- * @param {string} txt text message
- * @param {string} messageId current message id
- * @param {boolean} isCode set when message contains *code tag
- * @returns {string} formatted message
- */
-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="/$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="/$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/>' },
- ]);
-}
-/**
- * @external RegExpExecArray
- */
-
-/**
- * @callback MakeNodeCallback
- * @param { HTMLAnchorElement } aNode a DOM node of the link
- * @param { RegExpExecArray } reResult Result of RegExp execution
- * @param { HTMLDivElement} div target DOM element which can be updated by callback function
- * @returns { HTMLDivElement } updated DOM element
- */
-
-/**
- * @typedef { object } LinkFormatData
- * @property { string } id Format identifier
- * @property { string } name Format description
- * @property { RegExp } re Regular expression to match expected hyperlinks
- * @property { string } className list of CSS classes which
- * will be added to the target DOM element
- * @property { MakeNodeCallback } makeNode callback function called when a target link is matched
- */
-
-/**
- * Get supported embeddable formats
- *
- * @returns {LinkFormatData[]} list of supported formats
- */
-function getEmbeddableLinkTypes() {
- return [
- {
- name: 'Jpeg and png images',
- id: 'embed_jpeg_and_png_images',
- className: 'picture compact',
- re: /\.(jpe?g|png|svg)(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
- makeNode: function(aNode, reResult, div) {
- // dirty fix for dropbox urls
- let url = aNode.href.endsWith('dl=0') ? aNode.href.replace('dl=0', 'raw=1') : aNode.href;
- div.innerHTML = `<a href="${url}"><img src="${url}"></a>`;
- return div;
- }
- },
- {
- name: 'Gif images',
- id: 'embed_gif_images',
- className: 'picture compact',
- re: /\.gif(:[a-zA-Z]+)?(?:\?[\w&;\?=]*)?$/i,
- makeNode: function(aNode, reResult, div) {
- div.innerHTML = `<a href="${aNode.href}"><img src="${aNode.href}"></a>`;
- return div;
- }
- },
- {
- name: 'Video (webm, mp4, ogv)',
- id: 'embed_webm_and_mp4_videos',
- className: 'video compact',
- re: /\.(webm|mp4|m4v|ogv)(?:\?[\w&;\?=]*)?$/i,
- makeNode: function(aNode, reResult, div) {
- div.innerHTML = `<video src="${aNode.href}" title="${aNode.href}" controls></video>`;
- return div;
- }
- },
- {
- name: 'Audio (mp3, ogg, weba, opus, m4a, oga, wav)',
- id: 'embed_sound_files',
- className: 'audio singleColumn',
- re: /\.(mp3|ogg|weba|opus|m4a|oga|wav)(?:\?[\w&;\?=]*)?$/i,
- makeNode: function(aNode, reResult, div) {
- div.innerHTML = `<audio src="${aNode.href}" title="${aNode.href}" controls></audio>`;
- return div;
- }
- },
- {
- name: 'YouTube videos (and playlists)',
- id: 'embed_youtube_videos',
- className: 'youtube resizableV singleColumn',
- 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, div) {
- let [url, v, args, plist] = reResult;
- let iframeUrl;
- if (plist) {
- 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',
- enablejsapi: '1',
- origin: `${window.location.protocol}//${window.location.host}`
- };
- if (pp.t) {
- 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) {
- 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 setContent(div, iframe);
- }
- },
- {
- name: 'Vimeo videos',
- id: 'embed_vimeo_videos',
- className: 'vimeo resizableV',
- re: /^(?:https?:)?\/\/(?:www\.)?(?:player\.)?vimeo\.com\/(?:video\/|album\/[\d]+\/video\/)?([\d]+)/i,
- makeNode: function(aNode, reResult, div) {
- let iframe = makeIframe('//player.vimeo.com/video/' + reResult[1], '100%', '360px');
- iframe.onload = () => makeResizableToRatio(iframe, 9.0 / 16.0);
- return setContent(div, iframe);
- }
- },
- {
- name: 'Twitter',
- id: 'embed_twitter_status',
- className: 'twi compact',
- re: /^(?:https?:)?\/\/(?:www\.)?(?:mobile\.)?twitter\.com\/([\w-]+)\/status(?:es)?\/([\d]+)/i,
- makeNode: function(aNode, reResult, div) {
- fetch('https://beta.juick.com/api/oembed?url=' + reResult[0])
- .then(response => response.json())
- .then(json => {
- div.innerHTML = json.html;
- });
- return div;
- }
- },
- {
- name: 'Instagram media',
- id: 'embed_instagram_images',
- className: 'picture compact',
- re: /https?:\/\/www\.?instagram\.com(\/p\/\w+)\/?/i,
- makeNode: function(aNode, reResult, div) {
- let [url, postId] = reResult;
- let mediaUrl = `https://instagr.am${postId}/media`;
- div.innerHTML = `<a href="${aNode.href}"><img src="${mediaUrl}"></a>`;
- return div;
- }
- },
- {
- name: 'Telegram posts',
- id: 'embed_telegram_posts',
- className: 'tg compact',
- re: /https?:\/\/t\.me\/(\S+)/i,
- makeNode: function(aNode, reResult, div) {
- let [url, post] = reResult;
- // innerHTML cannot insert scripts, so...
- let script = document.createElement('script');
- script.src = 'https://telegram.org/js/telegram-widget.js?18';
- script.setAttribute('data-telegram-post', post);
- script.setAttribute('data-tme-mode', 'data-tme-mode');
- script.setAttribute('data-width', '100%');
- script.charset = 'utf-8';
- div.appendChild(script);
- return div;
- }
- },
- ];
-}
-
-/**
- * Embed a link
- *
- * @param { HTMLAnchorElement } aNode a DOM node of the link
- * @param { LinkFormatData[] } linkTypes supported link types
- * @param { HTMLElement } container a target DOM element with the link content
- * @param { boolean } afterNode where to insert new DOM node
- * @returns { boolean } `true` when some link was embedded
- */
-function embedLink(aNode, linkTypes, container, afterNode = false) {
- let anyEmbed = false;
- let linkId = (aNode.href.replace(/^https?:/i, '').replace(/\'/gi, ''));
- let sameEmbed = container.querySelector(`*[data-linkid='${linkId}']`); // do not embed the same thing twice
- if (!sameEmbed) {
- anyEmbed = linkTypes.some((linkType) => {
- let reResult = linkType.re.exec(aNode.href);
- if (reResult) {
- if (linkType.match && (linkType.match(aNode, reResult) === false)) { return false; }
- let newNode = makeNewNode(linkType, aNode, reResult);
- if (!newNode) { return false; }
- newNode.setAttribute('data-linkid', linkId);
- if (afterNode) {
- 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
- */
-export function embedLinksToX(x, beforeNodeSelector, allLinksSelector) {
- let isCtsPost = false;
- let allLinks = x.querySelectorAll(allLinksSelector);
-
- let existingContainer = x.querySelector('div.embedContainer');
- if (existingContainer) {
- 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);
- }
- }
-}
-
-/**
- * Embed all the links in all messages/replies on the page.
- */
-export function embedAll() {
- let beforeNodeSelector = '.msg-txt + *';
- let allLinksSelector = '.msg-txt a';
- Array.from(document.querySelectorAll('#content .msg-cont')).forEach(msg => {
- embedLinksToX(msg, beforeNodeSelector, allLinksSelector);
- });
-}
-
-export const format = juickFormat;
diff --git a/src/main/assets/scripts.js b/src/main/assets/scripts.js
index b204766d..27e2d59c 100644
--- a/src/main/assets/scripts.js
+++ b/src/main/assets/scripts.js
@@ -3,7 +3,7 @@ import 'formdata-polyfill';
import 'classlist.js';
import 'whatwg-fetch';
import 'core-js/stable';
-import { embedLinksToX, embedAll, format } from './embed';
+import { embedLinksToX, embedAll, format } from '../../../vnext/src/utils/embed';
import renderIcons from './icon';
import svg4everybody from 'svg4everybody';