From 380dad2b87d0e33b48b551519d24b80357e98765 Mon Sep 17 00:00:00 2001 From: Gabe Kangas Date: Tue, 2 Jun 2020 15:37:36 -0700 Subject: [PATCH] Support links and embeds in the chat --- webroot/index.html | 3 +- webroot/js/message.js | 4 + webroot/vendor/autolink.js | 238 +++++++++++++++++++++++++++++++++++++ 3 files changed, 244 insertions(+), 1 deletion(-) create mode 100644 webroot/vendor/autolink.js diff --git a/webroot/index.html b/webroot/index.html index 72dce4469..e3ef40aca 100644 --- a/webroot/index.html +++ b/webroot/index.html @@ -11,6 +11,7 @@ +
@@ -45,7 +46,7 @@

{{ message.author }}

-

{{ message.body }}

+

diff --git a/webroot/js/message.js b/webroot/js/message.js index 825d174da..b1ce1b282 100644 --- a/webroot/js/message.js +++ b/webroot/js/message.js @@ -5,6 +5,10 @@ class Message { this.image = "https://robohash.org/" + model.author } + linkedText() { + return autoLink(this.body, { embed: true }) + } + toModel() { return { author: this.author(), diff --git a/webroot/vendor/autolink.js b/webroot/vendor/autolink.js new file mode 100644 index 000000000..796b001f7 --- /dev/null +++ b/webroot/vendor/autolink.js @@ -0,0 +1,238 @@ +const re = { + http: /.*?:\/\//g, + url: /(\s|^)((https?|ftp):\/\/)?([\da-z\.-]+)\.([a-z\.]{2,6})([\w-\/\?\=\#\.])*/gi, + image: /\.(jpe?g|png|gif)$/, + email: /(([^<>()\[\]\\.,;:\s@"]+(\.[^<>()\[\]\\.,;:\s@"]+)*)|(".+"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))/gi, + cloudmusic: /http:\/\/music\.163\.com\/#\/song\?id=(\d+)/i, + kickstarter: /(https?:\/\/www\.kickstarter\.com\/projects\/\d+\/[a-zA-Z0-9_-]+)(\?\w+\=\w+)?/i, + youtube: /https?:\/\/www\.youtube\.com\/watch\?v=([a-zA-Z0-9_-]+)(\?\w+\=\w+)?/i, + vimeo: /https?:\/\/(?:www\.|player\.)?vimeo.com\/(?:channels\/(?:\w+\/)?|groups\/[^\/]*\/videos\/|album\/\d+\/video\/|video\/|)(\d+)(?:$|\/|\?)/i, + youku: /https?:\/\/v\.youku\.com\/v_show\/id_([a-zA-Z0-9_\=-]+).html(\?\w+\=\w+)?(\#\w+)?/i +} +/** + * AutoLink constructor function + * + * @param {String} text + * @param {Object} options + * @constructor + */ +function AutoLink(string, options = {}) { + this.string = options.safe ? safe_tags_replace(string) : string + this.options = options + this.attrs = '' + this.linkAttr = '' + this.imageAttr = '' + if (this.options.sharedAttr) { + this.attrs = getAttr(this.options.sharedAttr) + } + if (this.options.linkAttr) { + this.linkAttr = getAttr(this.options.linkAttr) + } + if (this.options.imageAttr) { + this.imageAttr = getAttr(this.options.imageAttr) + } +} + +AutoLink.prototype = { + constructor: AutoLink, + /** + * call relative functions to parse url/email/image + * + * @returns {String} + */ + parse: function() { + var shouldReplaceImage = defaultTrue(this.options.image) + var shouldRelaceEmail = defaultTrue(this.options.email) + var shouldRelaceBr = defaultTrue(this.options.br) + + var result = '' + if (!shouldReplaceImage) { + result = this.string.replace(re.url, this.formatURLMatch.bind(this)) + } else { + result = this.string.replace(re.url, this.formatIMGMatch.bind(this)) + } + if (shouldRelaceEmail) { + result = this.formatEmailMatch.call(this, result) + } + if (shouldRelaceBr) { + result = result.replace(/\r?\n/g, '
') + } + return result + }, + /** + * @param {String} match + * @returns {String} Offset 1 + */ + formatURLMatch: function(match, p1) { + match = prepHTTP(match.trim()) + if (this.options.cloudmusic || this.options.embed) { + if (match.indexOf('music.163.com/#/song?id=') > -1) { + return match.replace( + re.cloudmusic, + p1 + + '' + ) + } + } + if (this.options.kickstarter || this.options.embed) { + if (re.kickstarter.test(match)) { + return match.replace( + re.kickstarter, + p1 + + '' + ) + } + } + if (this.options.vimeo || this.options.embed) { + if (re.vimeo.test(match)) { + return match.replace( + re.vimeo, + p1 + + '' + ) + } + } + if (this.options.youtube || this.options.embed) { + if (re.youtube.test(match)) { + return match.replace( + re.youtube, + p1 + + '' + ) + } + } + if (this.options.youku || this.options.embed) { + if (re.youku.test(match)) { + return match.replace( + re.youku, + p1 + + '' + ) + } + } + var text = this.options.removeHTTP ? removeHTTP(match) : match + return ( + p1 + + '' + + text + + '' + ) + }, + /** + * @param {String} match + * @param {String} Offset 1 + */ + formatIMGMatch: function(match, p1) { + match = match.trim() + var isIMG = re.image.test(match) + if (isIMG) { + return ( + p1 + + '' + ) + } + return this.formatURLMatch(match, p1) + }, + /** + * @param {String} text + */ + formatEmailMatch: function(text) { + return text.replace( + re.email, + '$&' + ) + } +} + +/** + * return true if undefined + * else return itself + * + * @param {Boolean} val + * @returns {Boolean} + */ +function defaultTrue(val) { + return typeof val === 'undefined' ? true : val +} + +/** + * parse attrs from object + * + * @param {Object} obj + * @returns {Stirng} + */ +function getAttr(obj) { + var attr = '' + for (var key in obj) { + if (key) { + attr += ' ' + key + '="' + obj[key] + '"' + } + } + return attr +} + +/** + * @param {String} url + * @returns {String} + */ +function prepHTTP(url) { + if (url.substring(0, 4) !== 'http' && url.substring(0, 2) !== '//') { + return 'http://' + url + } + return url +} + +/** + * @param {String} url + * @returns {String} + */ +function removeHTTP(url) { + return url.replace(re.http, '') +} + +var tagsToReplace = { + '&': '&', + '<': '<', + '>': '>' +} + +/** + * Replace tag if should be replace + * + * @param {String} tag + * @returns {String} + */ +function replaceTag(tag) { + return tagsToReplace[tag] || tag +} + +/** + * Make string safe by replacing html tag + * + * @param {String} str + * @returns {String} + */ +function safe_tags_replace(str) { + return str.replace(/[&<>]/g, replaceTag) +} + +/** + * return an instance of AutoLink + * + * @param {String} string + * @param {Object} options + * @returns {Object} + */ +function autoLink(string, options) { + return new AutoLink(string, options).parse() +}