2020-09-25 17:35:24 +00:00
|
|
|
import Vue from 'vue'
|
2020-09-28 14:30:15 +00:00
|
|
|
import Emoji from './Emoji.vue'
|
2020-09-25 17:35:24 +00:00
|
|
|
|
|
|
|
export default Vue.component('MessageContent', {
|
|
|
|
props: {
|
|
|
|
source: {
|
|
|
|
type: Object,
|
|
|
|
required: true
|
|
|
|
}
|
|
|
|
},
|
|
|
|
render: function(createElement) {
|
|
|
|
return formatMessage(createElement, this.source)
|
|
|
|
}
|
|
|
|
})
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Transform the message source into Vue elements
|
|
|
|
*
|
|
|
|
* filters out all tags except <br/>, <p>, <span> and <a>.
|
|
|
|
*
|
|
|
|
* Links that are hashtags or mentions are rewritten to link to the local profile or hashtag page
|
|
|
|
* All external links have `rel="nofollow noopener noreferrer"` and `target="_blank"` set.
|
|
|
|
*
|
|
|
|
* All attributes other than `href` for links are stripped from the source
|
|
|
|
*/
|
|
|
|
export function formatMessage(createElement, source) {
|
2020-10-14 15:40:20 +00:00
|
|
|
if (!source.tag) {
|
|
|
|
source.tag = []
|
|
|
|
}
|
2020-09-25 17:35:24 +00:00
|
|
|
let mentions = source.tag.filter(tag => tag.type === 'Mention')
|
|
|
|
let hashtags = source.tag.filter(tag => tag.type === 'Hashtag')
|
|
|
|
|
|
|
|
let parser = new DOMParser()
|
|
|
|
let dom = parser.parseFromString(`<div id="rootwrapper">${source.content}</div>`, 'text/html')
|
|
|
|
let element = dom.getElementById('rootwrapper')
|
|
|
|
let cleaned = cleanCopy(createElement, element, { mentions, hashtags })
|
|
|
|
return cleaned
|
|
|
|
}
|
|
|
|
|
|
|
|
function domToVue(createElement, node, context) {
|
|
|
|
switch (node.tagName) {
|
|
|
|
case 'P':
|
|
|
|
return cleanCopy(createElement, node, context)
|
|
|
|
case 'BR':
|
|
|
|
return cleanCopy(createElement, node, context)
|
|
|
|
case 'SPAN':
|
|
|
|
return cleanCopy(createElement, node, context)
|
|
|
|
case 'A':
|
|
|
|
return cleanLink(createElement, node, context)
|
|
|
|
default:
|
2020-09-28 14:30:15 +00:00
|
|
|
return transformText(createElement, node.textContent)
|
2020-09-25 17:35:24 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2020-10-02 15:18:40 +00:00
|
|
|
const mentionRegex = /(\W|^)((@\w+)@[\w.-_]+)/i
|
|
|
|
const hashTagRegex = /(\W|^)(#\w+)/i
|
|
|
|
|
2020-09-28 14:30:15 +00:00
|
|
|
function transformText(createElement, text) {
|
2020-10-02 15:18:40 +00:00
|
|
|
return transformTextRegex(text, [
|
|
|
|
{
|
|
|
|
regex: mentionRegex,
|
|
|
|
onMatch: match => [
|
|
|
|
match[1],
|
|
|
|
createElement(
|
|
|
|
'router-link',
|
|
|
|
{
|
|
|
|
props: {
|
|
|
|
to: {
|
|
|
|
name: 'profile',
|
2022-03-24 15:17:46 +00:00
|
|
|
params: { account: match[2].slice(1) }
|
2020-10-09 16:16:40 +00:00
|
|
|
}
|
2020-10-02 15:18:40 +00:00
|
|
|
}
|
|
|
|
},
|
|
|
|
[match[3]]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
regex: hashTagRegex,
|
|
|
|
onMatch: match => [
|
|
|
|
match[1],
|
|
|
|
createElement(
|
|
|
|
'router-link',
|
|
|
|
{
|
|
|
|
props: {
|
|
|
|
to: {
|
|
|
|
name: 'tags',
|
2022-03-24 15:17:46 +00:00
|
|
|
params: { tag: match[2].slice(1) }
|
2020-10-02 15:18:40 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
},
|
|
|
|
[match[2]]
|
|
|
|
)
|
|
|
|
]
|
|
|
|
},
|
|
|
|
{
|
|
|
|
regex: emojiRe,
|
|
|
|
onMatch: match => createElement(
|
|
|
|
Emoji,
|
|
|
|
{
|
|
|
|
props: {
|
|
|
|
emoji: match[0]
|
|
|
|
}
|
|
|
|
}
|
|
|
|
)
|
|
|
|
}
|
|
|
|
])
|
2020-09-28 14:30:15 +00:00
|
|
|
}
|
|
|
|
|
2020-09-25 17:35:24 +00:00
|
|
|
/**
|
|
|
|
* copy a node without any attributes and cleaning all children
|
|
|
|
*/
|
|
|
|
function cleanCopy(createElement, node, context) {
|
|
|
|
let children = Array.from(node.childNodes).map(node => domToVue(createElement, node, context))
|
|
|
|
return createElement(node.tagName, children)
|
|
|
|
}
|
|
|
|
|
|
|
|
function cleanLink(createElement, node, context) {
|
|
|
|
let type = getLinkType(node.className)
|
|
|
|
let attributes = {}
|
|
|
|
|
|
|
|
switch (type) {
|
|
|
|
case 'mention':
|
2020-09-28 14:43:40 +00:00
|
|
|
var tag = matchMention(context.mentions, node.getAttribute('href'), node.textContent)
|
2020-09-25 17:35:24 +00:00
|
|
|
if (tag) {
|
2020-10-08 12:18:20 +00:00
|
|
|
attributes['rel'] = 'nofollow noopener noreferrer'
|
|
|
|
attributes['target'] = '_blank'
|
|
|
|
attributes['href'] = node.getAttribute('href')
|
|
|
|
attributes['title'] = tag.name
|
|
|
|
|
|
|
|
return createElement('a', { attrs: attributes }, [transformText(createElement, node.textContent)])
|
2020-09-25 17:35:24 +00:00
|
|
|
} else {
|
2020-09-28 14:30:15 +00:00
|
|
|
return transformText(createElement, node.textContent)
|
2020-09-25 17:35:24 +00:00
|
|
|
}
|
|
|
|
case 'hashtag':
|
2020-09-28 14:39:00 +00:00
|
|
|
return createElement(
|
|
|
|
'router-link',
|
2020-10-02 15:18:40 +00:00
|
|
|
{
|
|
|
|
props: {
|
|
|
|
to: {
|
|
|
|
name: 'tags',
|
2022-03-24 15:17:46 +00:00
|
|
|
params: { tag: node.textContent.slice(1) }
|
2020-10-02 15:18:40 +00:00
|
|
|
}
|
2020-09-28 14:39:00 +00:00
|
|
|
}
|
|
|
|
},
|
2020-10-02 15:18:40 +00:00
|
|
|
[node.textContent]
|
2020-09-28 14:39:00 +00:00
|
|
|
)
|
2020-09-25 17:35:24 +00:00
|
|
|
default:
|
|
|
|
attributes['rel'] = 'nofollow noopener noreferrer'
|
|
|
|
attributes['target'] = '_blank'
|
|
|
|
attributes['href'] = node.getAttribute('href')
|
|
|
|
|
2020-09-28 14:39:00 +00:00
|
|
|
return createElement('a', { attrs: attributes }, [transformText(createElement, node.textContent)])
|
|
|
|
}
|
2020-09-25 17:35:24 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
function getLinkType(className) {
|
|
|
|
let parts = className.split(' ')
|
|
|
|
if (parts.includes('hashtag')) {
|
|
|
|
return 'hashtag'
|
|
|
|
}
|
|
|
|
if (parts.includes('mention')) {
|
|
|
|
return 'mention'
|
|
|
|
}
|
|
|
|
return ''
|
|
|
|
}
|
|
|
|
|
|
|
|
function matchMention(tags, mentionHref, mentionText) {
|
|
|
|
let mentionUrl = new URL(mentionHref)
|
|
|
|
for (let tag of tags) {
|
|
|
|
if (mentionText === tag.name) {
|
|
|
|
return tag
|
|
|
|
}
|
|
|
|
|
|
|
|
// since the mention link href is not always equal to the href in the tag
|
|
|
|
// we instead match the server and username separate
|
|
|
|
let tagUrl = new URL(tag.href)
|
|
|
|
if (tagUrl.host === mentionUrl.host) {
|
|
|
|
let [, name] = tag.name.split('@')
|
|
|
|
if (name === mentionText || '@' + name === mentionText) {
|
|
|
|
return tag
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
return null
|
|
|
|
}
|
2020-09-28 14:30:15 +00:00
|
|
|
|
|
|
|
// RegExp based on emoji's official Unicode standards
|
|
|
|
// http://www.unicode.org/Public/UNIDATA/EmojiSources.txt
|
2020-10-02 15:18:40 +00:00
|
|
|
const emojiRe = /(?:\ud83d\udc68\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc68\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc68\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc68\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc68\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffc-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffd-\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb\udffc\udffe\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffd\udfff]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc68\ud83c[\udffb-\udffe]|\ud83d\udc69\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83d\udc69\ud83c[\udffb-\udffe]|\ud83e\uddd1\ud83c\udffb\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffc\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffd\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udffe\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\ud83c\udfff\u200d\ud83e\udd1d\u200d\ud83e\uddd1\ud83c[\udffb-\udfff]|\ud83e\uddd1\u200d\ud83e\udd1d\u200d\ud83e\uddd1|\ud83d\udc6b\ud83c[\udffb-\udfff]|\ud83d\udc6c\ud83c[\udffb-\udfff]|\ud83d\udc6d\ud83c[\udffb-\udfff]|\ud83d[\udc6b-\udc6d])|(?:\ud83d[\udc68\udc69]|\ud83e\uddd1)(?:\ud83c[\udffb-\udfff])?\u200d(?:\u2695\ufe0f|\u2696\ufe0f|\u2708\ufe0f|\ud83c[\udf3e\udf73\udf7c\udf84\udf93\udfa4\udfa8\udfeb\udfed]|\ud83d[\udcbb\udcbc\udd27\udd2c\ude80\ude92]|\ud83e[\uddaf-\uddb3\uddbc\uddbd])|(?:\ud83c[\udfcb\udfcc]|\ud83d[\udd74\udd75]|\u26f9)((?:\ud83c[\udffb-\udfff]|\ufe0f)\u200d[\u2640\u2642]\ufe0f)|(?:\ud83c[\udfc3\udfc4\udfca]|\ud83d[\udc6e\udc70\udc71\udc73\udc77\udc81\udc82\udc86\udc87\ude45-\ude47\ude4b\ude4d\ude4e\udea3\udeb4-\udeb6]|\ud83e[\udd26\udd35\udd37-\udd39\udd3d\udd3e\uddb8\uddb9\uddcd-\uddcf\uddd6-\udddd])(?:\ud83c[\udffb-\udfff])?\u200d[\u2640\u2642]\ufe0f|(?:\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d\udc8b\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\u2764\ufe0f\u200d\ud83d\udc68|\ud83d\udc68\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc68\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc68\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\u2764\ufe0f\u200d\ud83d[\udc68\udc69]|\ud83d\udc69\u200d\ud83d\udc66\u200d\ud83d\udc66|\ud83d\udc69\u200d\ud83d\udc67\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83c\udff3\ufe0f\u200d\u26a7\ufe0f|\ud83c\udff3\ufe0f\u200d\ud83c\udf08|\ud83c\udff4\u200d\u2620\ufe0f|\ud83d\udc15\u200d\ud83e\uddba|\ud83d\udc3b\u200d\u2744\ufe0f|\ud83d\udc41\u200d\ud83d\udde8|\ud83d\udc68\u200d\ud83d[\udc66\udc67]|\ud83d\udc69\u200d\ud83d[\udc66\udc67]|\ud83d\udc6f\u200d\u2640\ufe0f|\ud83d\udc6f\u200d\u2642\ufe0f|\ud83e\udd3c\u200d\u2640\ufe0f|\ud83e\udd3c\u200d\u2642\ufe0f|\ud83e\uddde\u200d\u2640\ufe0f|\ud83e\uddde\u200d\u2642\ufe0f|\ud83e\udddf\u200d\u264
|
2020-09-28 14:30:15 +00:00
|
|
|
|
2020-10-02 15:18:40 +00:00
|
|
|
function transformTextRegex(text, handlers) {
|
2020-09-28 14:43:40 +00:00
|
|
|
let parts = []
|
2020-09-28 14:30:15 +00:00
|
|
|
|
2020-10-02 15:18:40 +00:00
|
|
|
while (text.length > 0) {
|
|
|
|
let result = handlers.reduce((bestMatch, handler) => {
|
|
|
|
let match
|
|
|
|
if ((match = handler.regex.exec(text))) {
|
|
|
|
if (bestMatch.index === -1 || match.index < bestMatch.index) {
|
|
|
|
return {
|
|
|
|
index: match.index,
|
|
|
|
match,
|
|
|
|
onMatch: handler.onMatch
|
|
|
|
}
|
2020-09-28 14:30:15 +00:00
|
|
|
}
|
|
|
|
}
|
2020-10-02 15:18:40 +00:00
|
|
|
return bestMatch
|
|
|
|
}, { index: -1 })
|
|
|
|
|
|
|
|
if (result.index !== -1) {
|
|
|
|
if (result.index > 0) {
|
|
|
|
parts.push(text.slice(0, result.index))
|
|
|
|
}
|
|
|
|
|
|
|
|
parts.push(result.onMatch(result.match))
|
|
|
|
text = text.slice(result.index + result.match[0].length)
|
|
|
|
} else {
|
|
|
|
parts.push(text)
|
|
|
|
return parts
|
|
|
|
}
|
2020-09-28 14:30:15 +00:00
|
|
|
}
|
|
|
|
|
2020-09-28 14:43:40 +00:00
|
|
|
return parts
|
2020-09-28 14:30:15 +00:00
|
|
|
}
|