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()
+}