|
|
|
|
import {encodeEntities, copy, emojiUnicode} from './utils';
|
|
|
|
|
import Config from './config';
|
|
|
|
|
|
|
|
|
|
var EmojiHelper = {
|
|
|
|
|
emojiMap: (code) => { return code; },
|
|
|
|
|
shortcuts: [],
|
|
|
|
|
emojis: []
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
var emojiData = Config.Emoji;
|
|
|
|
|
var emojiSupported = navigator.userAgent.search(/OS X|iPhone|iPad|iOS/i) != -1/* && false */,
|
|
|
|
|
emojiCode;
|
|
|
|
|
|
|
|
|
|
// added * to (?:[©®\\u2122\\u265f]\\ufe0f) and removed \\ufe0f from end
|
|
|
|
|
// there are no 2640 and 2642 (signs)
|
|
|
|
|
var emojiRegExp = '(?:\\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[\\u
|
|
|
|
|
var alphaCharsRegExp = 'a-z' +
|
|
|
|
|
'\\u00c0-\\u00d6\\u00d8-\\u00f6\\u00f8-\\u00ff' + // Latin-1
|
|
|
|
|
'\\u0100-\\u024f' + // Latin Extended A and B
|
|
|
|
|
'\\u0253\\u0254\\u0256\\u0257\\u0259\\u025b\\u0263\\u0268\\u026f\\u0272\\u0289\\u028b' + // IPA Extensions
|
|
|
|
|
'\\u02bb' + // Hawaiian
|
|
|
|
|
'\\u0300-\\u036f' + // Combining diacritics
|
|
|
|
|
'\\u1e00-\\u1eff' + // Latin Extended Additional (mostly for Vietnamese)
|
|
|
|
|
'\\u0400-\\u04ff\\u0500-\\u0527' + // Cyrillic
|
|
|
|
|
'\\u2de0-\\u2dff\\ua640-\\ua69f' + // Cyrillic Extended A/B
|
|
|
|
|
'\\u0591-\\u05bf\\u05c1-\\u05c2\\u05c4-\\u05c5\\u05c7' +
|
|
|
|
|
'\\u05d0-\\u05ea\\u05f0-\\u05f4' + // Hebrew
|
|
|
|
|
'\\ufb1d-\\ufb28\\ufb2a-\\ufb36\\ufb38-\\ufb3c\\ufb3e\\ufb40-\\ufb41' +
|
|
|
|
|
'\\ufb43-\\ufb44\\ufb46-\\ufb4f' + // Hebrew Pres. Forms
|
|
|
|
|
'\\u0610-\\u061a\\u0620-\\u065f\\u066e-\\u06d3\\u06d5-\\u06dc' +
|
|
|
|
|
'\\u06de-\\u06e8\\u06ea-\\u06ef\\u06fa-\\u06fc\\u06ff' + // Arabic
|
|
|
|
|
'\\u0750-\\u077f\\u08a0\\u08a2-\\u08ac\\u08e4-\\u08fe' + // Arabic Supplement and Extended A
|
|
|
|
|
'\\ufb50-\\ufbb1\\ufbd3-\\ufd3d\\ufd50-\\ufd8f\\ufd92-\\ufdc7\\ufdf0-\\ufdfb' + // Pres. Forms A
|
|
|
|
|
'\\ufe70-\\ufe74\\ufe76-\\ufefc' + // Pres. Forms B
|
|
|
|
|
'\\u200c' + // Zero-Width Non-Joiner
|
|
|
|
|
'\\u0e01-\\u0e3a\\u0e40-\\u0e4e' + // Thai
|
|
|
|
|
'\\u1100-\\u11ff\\u3130-\\u3185\\uA960-\\uA97F\\uAC00-\\uD7AF\\uD7B0-\\uD7FF' + // Hangul (Korean)
|
|
|
|
|
'\\u3003\\u3005\\u303b' + // Kanji/Han iteration marks
|
|
|
|
|
'\\uff21-\\uff3a\\uff41-\\uff5a' + // full width Alphabet
|
|
|
|
|
'\\uff66-\\uff9f' + // half width Katakana
|
|
|
|
|
'\\uffa1-\\uffdc'; // half width Hangul (Korean)
|
|
|
|
|
var alphaNumericRegExp = '0-9\_' + alphaCharsRegExp;
|
|
|
|
|
var domainAddChars = '\u00b7';
|
|
|
|
|
// Based on Regular Expression for URL validation by Diego Perini
|
|
|
|
|
var urlRegExp = '((?:https?|ftp)://|mailto:)?' +
|
|
|
|
|
// user:pass authentication
|
|
|
|
|
'(?:\\S{1,64}(?::\\S{0,64})?@)?' +
|
|
|
|
|
'(?:' +
|
|
|
|
|
// sindresorhus/ip-regexp
|
|
|
|
|
'(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])(?:\\.(?:25[0-5]|2[0-4][0-9]|1[0-9][0-9]|[1-9][0-9]|[0-9])){3}' +
|
|
|
|
|
'|' +
|
|
|
|
|
// host name
|
|
|
|
|
'[' + alphaCharsRegExp + '0-9][' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}' +
|
|
|
|
|
// domain name
|
|
|
|
|
'(?:\\.[' + alphaCharsRegExp + '0-9][' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}){0,10}' +
|
|
|
|
|
// TLD identifier
|
|
|
|
|
'(?:\\.(xn--[0-9a-z]{2,16}|[' + alphaCharsRegExp + ']{2,24}))' +
|
|
|
|
|
')' +
|
|
|
|
|
// port number
|
|
|
|
|
'(?::\\d{2,5})?' +
|
|
|
|
|
// resource path
|
|
|
|
|
'(?:/(?:\\S{0,255}[^\\s.;,(\\[\\]{}<>"\'])?)?'
|
|
|
|
|
var usernameRegExp = '[a-zA-Z\\d_]{5,32}'
|
|
|
|
|
var botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?(\\b|$)'
|
|
|
|
|
var fullRegExp = new RegExp('(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp, 'i')
|
|
|
|
|
var emailRegExp = /^(([^<>()[\]\\.,;:\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,}))$/
|
|
|
|
|
var markdownTestRegExp = /[`_*@]/
|
|
|
|
|
var markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s)(`|\*\*|__)([^\n]+?)\7([\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)/m
|
|
|
|
|
var siteHashtags = {
|
|
|
|
|
Telegram: 'tg://search_hashtag?hashtag={1}',
|
|
|
|
|
Twitter: 'https://twitter.com/hashtag/{1}',
|
|
|
|
|
Instagram: 'https://instagram.com/explore/tags/{1}/',
|
|
|
|
|
'Google Plus': 'https://plus.google.com/explore/{1}'
|
|
|
|
|
}
|
|
|
|
|
var siteMentions = {
|
|
|
|
|
Telegram: '#/im?p=%40{1}',
|
|
|
|
|
Twitter: 'https://twitter.com/{1}',
|
|
|
|
|
Instagram: 'https://instagram.com/{1}/',
|
|
|
|
|
GitHub: 'https://github.com/{1}'
|
|
|
|
|
}
|
|
|
|
|
var markdownEntities = {
|
|
|
|
|
'`': 'messageEntityCode',
|
|
|
|
|
'**': 'messageEntityBold',
|
|
|
|
|
'__': 'messageEntityItalic'
|
|
|
|
|
}
|
|
|
|
|
function getEmojiSpritesheetCoords(emojiCode) {
|
|
|
|
|
let emojiInfo = emojiData[emojiCode/* .replace(/\ufe0f/g, '') */];
|
|
|
|
|
if(emojiInfo === undefined) {
|
|
|
|
|
//console.error('no emoji by code:', emojiCode, emojiCode && emojiCode.length, new TextEncoder().encode(emojiCode), emojiUnicode(emojiCode));
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return emojiUnicode(emojiCode).replace(/(-fe0f|fe0f)/g, '');
|
|
|
|
|
}
|
|
|
|
|
function parseEntities(text, options = {}) {
|
|
|
|
|
var match;
|
|
|
|
|
var raw = text, url;
|
|
|
|
|
var entities = [], matchIndex;
|
|
|
|
|
var rawOffset = 0;
|
|
|
|
|
// var start = tsNow()
|
|
|
|
|
while((match = raw.match(fullRegExp))) {
|
|
|
|
|
matchIndex = rawOffset + match.index;
|
|
|
|
|
|
|
|
|
|
//console.log('parseEntities match:', match);
|
|
|
|
|
|
|
|
|
|
if(match[3]) { // mentions
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityMention',
|
|
|
|
|
offset: matchIndex + match[1].length,
|
|
|
|
|
length: match[2].length + match[3].length
|
|
|
|
|
});
|
|
|
|
|
} else if(match[4]) {
|
|
|
|
|
if(emailRegExp.test(match[4])) { // email
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityEmail',
|
|
|
|
|
offset: matchIndex,
|
|
|
|
|
length: match[4].length
|
|
|
|
|
});
|
|
|
|
|
} else {
|
|
|
|
|
var url = false;
|
|
|
|
|
var protocol = match[5];
|
|
|
|
|
var tld = match[6];
|
|
|
|
|
var excluded = '';
|
|
|
|
|
if(tld) { // URL
|
|
|
|
|
if(!protocol && (tld.substr(0, 4) === 'xn--' || Config.TLD.indexOf(tld.toLowerCase()) !== -1)) {
|
|
|
|
|
protocol = 'http://';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(protocol) {
|
|
|
|
|
var balanced = checkBrackets(match[4]);
|
|
|
|
|
if (balanced.length !== match[4].length) {
|
|
|
|
|
excluded = match[4].substring(balanced.length);
|
|
|
|
|
match[4] = balanced;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
url = (match[5] ? '' : protocol) + match[4];
|
|
|
|
|
}
|
|
|
|
|
} else { // IP address
|
|
|
|
|
url = (match[5] ? '' : 'http://') + match[4];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (url) {
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityUrl',
|
|
|
|
|
offset: matchIndex,
|
|
|
|
|
length: match[4].length
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else if(match[7]) { // New line
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityLinebreak',
|
|
|
|
|
offset: matchIndex,
|
|
|
|
|
length: 1
|
|
|
|
|
});
|
|
|
|
|
} else if(match[8]) { // Emoji
|
|
|
|
|
//console.log('hit', match[8]);
|
|
|
|
|
let emojiCoords = getEmojiSpritesheetCoords(match[8]);
|
|
|
|
|
if(emojiCoords) {
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityEmoji',
|
|
|
|
|
offset: matchIndex,
|
|
|
|
|
length: match[8].length,
|
|
|
|
|
unicode: emojiCoords
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
} else if(match[10]) { // Hashtag
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityHashtag',
|
|
|
|
|
offset: matchIndex + (match[9] ? match[9].length : 0),
|
|
|
|
|
length: match[10].length
|
|
|
|
|
});
|
|
|
|
|
} else if(match[12]) { // Bot command
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityBotCommand',
|
|
|
|
|
offset: matchIndex + (match[11] ? match[11].length : 0),
|
|
|
|
|
length: 1 + match[12].length + (match[13] ? 1 + match[13].length : 0)
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
raw = raw.substr(match.index + match[0].length);
|
|
|
|
|
rawOffset += match.index + match[0].length;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// if (entities.length) {
|
|
|
|
|
// console.log('parse entities', text, entities.slice())
|
|
|
|
|
// }
|
|
|
|
|
return entities
|
|
|
|
|
}
|
|
|
|
|
function parseEmojis (text) {
|
|
|
|
|
return text.replace(/:([a-z0-9\-\+\*_]+?):/gi, function (all, shortcut) {
|
|
|
|
|
var emojiCode = EmojiHelper.shortcuts[shortcut]
|
|
|
|
|
if (emojiCode !== undefined) {
|
|
|
|
|
return EmojiHelper.emojis[emojiCode][0]
|
|
|
|
|
}
|
|
|
|
|
return all
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
function parseMarkdown (text, entities, noTrim) {
|
|
|
|
|
if (!markdownTestRegExp.test(text)) {
|
|
|
|
|
return noTrim ? text : text.trim()
|
|
|
|
|
}
|
|
|
|
|
var raw = text
|
|
|
|
|
var match
|
|
|
|
|
var newText = []
|
|
|
|
|
var rawOffset = 0
|
|
|
|
|
var matchIndex
|
|
|
|
|
while (match = raw.match(markdownRegExp)) {
|
|
|
|
|
matchIndex = rawOffset + match.index
|
|
|
|
|
newText.push(raw.substr(0, match.index))
|
|
|
|
|
var text = (match[3] || match[8] || match[11])
|
|
|
|
|
rawOffset -= text.length
|
|
|
|
|
text = text.replace(/^\s+|\s+$/g, '')
|
|
|
|
|
rawOffset += text.length
|
|
|
|
|
if (text.match(/^`*$/)) {
|
|
|
|
|
newText.push(match[0])
|
|
|
|
|
}
|
|
|
|
|
else if (match[3]) { // pre
|
|
|
|
|
if (match[5] == '\n') {
|
|
|
|
|
match[5] = ''
|
|
|
|
|
rawOffset -= 1
|
|
|
|
|
}
|
|
|
|
|
newText.push(match[1] + text + match[5])
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityPre',
|
|
|
|
|
language: '',
|
|
|
|
|
offset: matchIndex + match[1].length,
|
|
|
|
|
length: text.length
|
|
|
|
|
})
|
|
|
|
|
rawOffset -= match[2].length + match[4].length
|
|
|
|
|
} else if (match[7]) { // code|italic|bold
|
|
|
|
|
newText.push(match[6] + text + match[9])
|
|
|
|
|
entities.push({
|
|
|
|
|
_: markdownEntities[match[7]],
|
|
|
|
|
offset: matchIndex + match[6].length,
|
|
|
|
|
length: text.length
|
|
|
|
|
})
|
|
|
|
|
rawOffset -= match[7].length * 2
|
|
|
|
|
} else if (match[11]) { // custom mention
|
|
|
|
|
newText.push(text)
|
|
|
|
|
entities.push({
|
|
|
|
|
_: 'messageEntityMentionName',
|
|
|
|
|
user_id: match[10],
|
|
|
|
|
offset: matchIndex,
|
|
|
|
|
length: text.length
|
|
|
|
|
})
|
|
|
|
|
rawOffset -= match[0].length - text.length
|
|
|
|
|
}
|
|
|
|
|
raw = raw.substr(match.index + match[0].length)
|
|
|
|
|
rawOffset += match.index + match[0].length
|
|
|
|
|
}
|
|
|
|
|
newText.push(raw)
|
|
|
|
|
newText = newText.join('')
|
|
|
|
|
if (!newText.replace(/\s+/g, '').length) {
|
|
|
|
|
newText = text
|
|
|
|
|
entities.splice(0, entities.length)
|
|
|
|
|
}
|
|
|
|
|
if (!entities.length && !noTrim) {
|
|
|
|
|
newText = newText.trim()
|
|
|
|
|
}
|
|
|
|
|
return newText
|
|
|
|
|
}
|
|
|
|
|
function mergeEntities (currentEntities, newEntities, fromApi) {
|
|
|
|
|
var totalEntities = newEntities.slice()
|
|
|
|
|
var i
|
|
|
|
|
var len = currentEntities.length
|
|
|
|
|
var j
|
|
|
|
|
var len2 = newEntities.length
|
|
|
|
|
var startJ = 0
|
|
|
|
|
var curEntity
|
|
|
|
|
var newEntity
|
|
|
|
|
var start, end
|
|
|
|
|
var cStart, cEnd
|
|
|
|
|
var bad
|
|
|
|
|
for (i = 0; i < len; i++) {
|
|
|
|
|
curEntity = currentEntities[i]
|
|
|
|
|
if (fromApi &&
|
|
|
|
|
curEntity._ != 'messageEntityLinebreak' &&
|
|
|
|
|
curEntity._ != 'messageEntityEmoji') {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
// console.log('s', curEntity, newEntities)
|
|
|
|
|
start = curEntity.offset
|
|
|
|
|
end = start + curEntity.length
|
|
|
|
|
bad = false
|
|
|
|
|
for (j = startJ; j < len2; j++) {
|
|
|
|
|
newEntity = newEntities[j]
|
|
|
|
|
cStart = newEntity.offset
|
|
|
|
|
cEnd = cStart + newEntity.length
|
|
|
|
|
if (cStart <= start) {
|
|
|
|
|
startJ = j
|
|
|
|
|
}
|
|
|
|
|
if (start >= cStart && start < cEnd ||
|
|
|
|
|
end > cStart && end <= cEnd) {
|
|
|
|
|
// console.log('bad', curEntity, newEntity)
|
|
|
|
|
if (fromApi &&
|
|
|
|
|
start >= cStart && end <= cEnd) {
|
|
|
|
|
if (newEntity.nested === undefined) {
|
|
|
|
|
newEntity.nested = []
|
|
|
|
|
}
|
|
|
|
|
curEntity.offset -= cStart
|
|
|
|
|
newEntity.nested.push(copy(curEntity))
|
|
|
|
|
}
|
|
|
|
|
bad = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
if (cStart >= end) {
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if (bad) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
totalEntities.push(curEntity)
|
|
|
|
|
}
|
|
|
|
|
totalEntities.sort(function (a, b) {
|
|
|
|
|
return a.offset - b.offset
|
|
|
|
|
})
|
|
|
|
|
// console.log('merge', currentEntities, newEntities, totalEntities)
|
|
|
|
|
return totalEntities
|
|
|
|
|
}
|
|
|
|
|
function wrapRichNestedText (text, nested, options) {
|
|
|
|
|
if (nested === undefined) {
|
|
|
|
|
return encodeEntities(text)
|
|
|
|
|
}
|
|
|
|
|
options.hasNested = true
|
|
|
|
|
return wrapRichText(text, {entities: nested, nested: true})
|
|
|
|
|
}
|
|
|
|
|
function wrapRichText (text, options = {}) {
|
|
|
|
|
if(!text || !text.length) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var entities = options.entities;
|
|
|
|
|
var contextSite = options.contextSite || 'Telegram';
|
|
|
|
|
var contextExternal = contextSite != 'Telegram';
|
|
|
|
|
var emojiFound = false;
|
|
|
|
|
if(entities === undefined) {
|
|
|
|
|
entities = parseEntities(text, options);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
//console.log('wrapRichText got entities:', text, entities);
|
|
|
|
|
var len = entities.length;
|
|
|
|
|
var entity;
|
|
|
|
|
var entityText;
|
|
|
|
|
var skipEntity;
|
|
|
|
|
var url;
|
|
|
|
|
var html = [];
|
|
|
|
|
var lastOffset = 0;
|
|
|
|
|
for(var i = 0; i < len; i++) {
|
|
|
|
|
entity = entities[i];
|
|
|
|
|
if(entity.offset > lastOffset) {
|
|
|
|
|
html.push(
|
|
|
|
|
encodeEntities(text.substr(lastOffset, entity.offset - lastOffset))
|
|
|
|
|
);
|
|
|
|
|
} else if(entity.offset < lastOffset) {
|
|
|
|
|
continue;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
skipEntity = false;
|
|
|
|
|
entityText = text.substr(entity.offset, entity.length);
|
|
|
|
|
switch(entity._) {
|
|
|
|
|
case 'messageEntityMention':
|
|
|
|
|
var contextUrl = !options.noLinks && siteMentions[contextSite]
|
|
|
|
|
if (!contextUrl) {
|
|
|
|
|
skipEntity = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
var username = entityText.substr(1)
|
|
|
|
|
var attr = ''
|
|
|
|
|
if (options.highlightUsername &&
|
|
|
|
|
options.highlightUsername.toLowerCase() == username.toLowerCase()) {
|
|
|
|
|
attr = 'class="im_message_mymention"'
|
|
|
|
|
}
|
|
|
|
|
html.push(
|
|
|
|
|
'<a ',
|
|
|
|
|
attr,
|
|
|
|
|
contextExternal ? ' target="_blank" rel="noopener noreferrer" ' : '',
|
|
|
|
|
' href="',
|
|
|
|
|
contextUrl.replace('{1}', encodeURIComponent(username)),
|
|
|
|
|
'">',
|
|
|
|
|
encodeEntities(entityText),
|
|
|
|
|
'</a>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityMentionName':
|
|
|
|
|
if (options.noLinks) {
|
|
|
|
|
skipEntity = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
html.push(
|
|
|
|
|
'<a href="#/im?p=u',
|
|
|
|
|
encodeURIComponent(entity.user_id),
|
|
|
|
|
'">',
|
|
|
|
|
encodeEntities(entityText),
|
|
|
|
|
'</a>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityHashtag':
|
|
|
|
|
var contextUrl = !options.noLinks && siteHashtags[contextSite]
|
|
|
|
|
if (!contextUrl) {
|
|
|
|
|
skipEntity = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
var hashtag = entityText.substr(1)
|
|
|
|
|
html.push(
|
|
|
|
|
'<a ',
|
|
|
|
|
contextExternal ? ' target="_blank" rel="noopener noreferrer" ' : '',
|
|
|
|
|
'href="',
|
|
|
|
|
contextUrl.replace('{1}', encodeURIComponent(hashtag))
|
|
|
|
|
,
|
|
|
|
|
'">',
|
|
|
|
|
encodeEntities(entityText),
|
|
|
|
|
'</a>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityEmail':
|
|
|
|
|
if (options.noLinks) {
|
|
|
|
|
skipEntity = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
html.push(
|
|
|
|
|
'<a href="',
|
|
|
|
|
encodeEntities('mailto:' + entityText),
|
|
|
|
|
'" target="_blank" rel="noopener noreferrer">',
|
|
|
|
|
encodeEntities(entityText),
|
|
|
|
|
'</a>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityUrl':
|
|
|
|
|
case 'messageEntityTextUrl':
|
|
|
|
|
var inner
|
|
|
|
|
if (entity._ == 'messageEntityTextUrl') {
|
|
|
|
|
url = entity.url
|
|
|
|
|
url = wrapUrl(url, true)
|
|
|
|
|
inner = wrapRichNestedText(entityText, entity.nested, options)
|
|
|
|
|
} else {
|
|
|
|
|
url = wrapUrl(entityText, false)
|
|
|
|
|
inner = encodeEntities(replaceUrlEncodings(entityText))
|
|
|
|
|
}
|
|
|
|
|
if (options.noLinks) {
|
|
|
|
|
html.push(inner);
|
|
|
|
|
} else {
|
|
|
|
|
html.push(
|
|
|
|
|
'<a href="',
|
|
|
|
|
encodeEntities(url),
|
|
|
|
|
'" target="_blank" rel="noopener noreferrer">',
|
|
|
|
|
inner,
|
|
|
|
|
'</a>'
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityLinebreak':
|
|
|
|
|
html.push(options.noLinebreaks ? ' ' : '<br/>')
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityEmoji':
|
|
|
|
|
html.push(emojiSupported ?
|
|
|
|
|
`<span class="emoji" contenteditable="false">${encodeEntities(entityText)}</span>` :
|
|
|
|
|
`<img src="assets/img/emoji/${entity.unicode}.png" alt="${encodeEntities(entityText)}" class="emoji">`);
|
|
|
|
|
|
|
|
|
|
emojiFound = true;
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityBotCommand':
|
|
|
|
|
if (options.noLinks || options.noCommands || contextExternal) {
|
|
|
|
|
skipEntity = true
|
|
|
|
|
break
|
|
|
|
|
}
|
|
|
|
|
var command = entityText.substr(1)
|
|
|
|
|
var bot
|
|
|
|
|
var atPos
|
|
|
|
|
if ((atPos = command.indexOf('@')) != -1) {
|
|
|
|
|
bot = command.substr(atPos + 1)
|
|
|
|
|
command = command.substr(0, atPos)
|
|
|
|
|
} else {
|
|
|
|
|
bot = options.fromBot
|
|
|
|
|
}
|
|
|
|
|
html.push(
|
|
|
|
|
'<a href="',
|
|
|
|
|
encodeEntities('tg://bot_command?command=' + encodeURIComponent(command) + (bot ? '&bot=' + encodeURIComponent(bot) : '')),
|
|
|
|
|
'">',
|
|
|
|
|
encodeEntities(entityText),
|
|
|
|
|
'</a>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityBold':
|
|
|
|
|
if(options.noTextFormat) {
|
|
|
|
|
html.push(wrapRichNestedText(entityText, entity.nested, options));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.push(
|
|
|
|
|
'<strong>',
|
|
|
|
|
wrapRichNestedText(entityText, entity.nested, options),
|
|
|
|
|
'</strong>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityItalic':
|
|
|
|
|
if(options.noTextFormat) {
|
|
|
|
|
html.push(wrapRichNestedText(entityText, entity.nested, options));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.push(
|
|
|
|
|
'<em>',
|
|
|
|
|
wrapRichNestedText(entityText, entity.nested, options),
|
|
|
|
|
'</em>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityHighlight':
|
|
|
|
|
html.push(
|
|
|
|
|
'<i>',
|
|
|
|
|
wrapRichNestedText(entityText, entity.nested, options),
|
|
|
|
|
'</i>'
|
|
|
|
|
)
|
|
|
|
|
break;
|
|
|
|
|
case 'messageEntityCode':
|
|
|
|
|
if(options.noTextFormat) {
|
|
|
|
|
html.push(encodeEntities(entityText));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.push(
|
|
|
|
|
'<code>',
|
|
|
|
|
encodeEntities(entityText),
|
|
|
|
|
'</code>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityPre':
|
|
|
|
|
if(options.noTextFormat) {
|
|
|
|
|
html.push(encodeEntities(entityText));
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.push(
|
|
|
|
|
'<pre><code', (entity.language ? ' class="language-' + encodeEntities(entity.language) + '"' : ''), '>',
|
|
|
|
|
encodeEntities(entityText),
|
|
|
|
|
'</code></pre>'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
skipEntity = true
|
|
|
|
|
}
|
|
|
|
|
lastOffset = entity.offset + (skipEntity ? 0 : entity.length)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
html.push(encodeEntities(text.substr(lastOffset))); // may be empty string
|
|
|
|
|
//console.log(html);
|
|
|
|
|
text = html.join('')//$sanitize(html.join(''))
|
|
|
|
|
/* if (!options.nested && (emojiFound || options.hasNested)) {
|
|
|
|
|
text = text.replace(/\ufe0f|️|�|‍/g, '', text)
|
|
|
|
|
var emojiSizeClass = curEmojiSize == 18 ? '' : (' emoji-w' + curEmojiSize)
|
|
|
|
|
text = text.replace(/<span((?: [^>]*)?) class="emoji emoji-(\d)-(\d+)-(\d+)"(.+?)<\/span>/g,
|
|
|
|
|
'<span$1 class="emoji ' + emojiSizeClass + ' emoji-spritesheet-$2" style="background-position: -$3px -$4px;" $5</span>')
|
|
|
|
|
} */
|
|
|
|
|
return text;//$sce.trustAs('html', text)
|
|
|
|
|
}
|
|
|
|
|
function wrapDraftText (text, options) {
|
|
|
|
|
if (!text || !text.length) {
|
|
|
|
|
return ''
|
|
|
|
|
}
|
|
|
|
|
options = options || {}
|
|
|
|
|
var entities = options.entities
|
|
|
|
|
if (entities === undefined) {
|
|
|
|
|
entities = parseEntities(text, options)
|
|
|
|
|
}
|
|
|
|
|
var i = 0
|
|
|
|
|
var len = entities.length
|
|
|
|
|
var entity
|
|
|
|
|
var entityText
|
|
|
|
|
var skipEntity
|
|
|
|
|
var code = []
|
|
|
|
|
var lastOffset = 0
|
|
|
|
|
for (i = 0; i < len; i++) {
|
|
|
|
|
entity = entities[i]
|
|
|
|
|
if (entity.offset > lastOffset) {
|
|
|
|
|
code.push(
|
|
|
|
|
text.substr(lastOffset, entity.offset - lastOffset)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
else if (entity.offset < lastOffset) {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
skipEntity = false
|
|
|
|
|
entityText = text.substr(entity.offset, entity.length)
|
|
|
|
|
switch (entity._) {
|
|
|
|
|
case 'messageEntityEmoji':
|
|
|
|
|
code.push(
|
|
|
|
|
':',
|
|
|
|
|
entity.title,
|
|
|
|
|
':'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityCode':
|
|
|
|
|
code.push(
|
|
|
|
|
'`', entityText, '`'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityBold':
|
|
|
|
|
code.push(
|
|
|
|
|
'**', entityText, '**'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityItalic':
|
|
|
|
|
code.push(
|
|
|
|
|
'__', entityText, '__'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityPre':
|
|
|
|
|
code.push(
|
|
|
|
|
'```', entityText, '```'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
case 'messageEntityMentionName':
|
|
|
|
|
code.push(
|
|
|
|
|
'@', entity.user_id, ' (', entityText, ')'
|
|
|
|
|
)
|
|
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
skipEntity = true
|
|
|
|
|
}
|
|
|
|
|
lastOffset = entity.offset + (skipEntity ? 0 : entity.length)
|
|
|
|
|
}
|
|
|
|
|
code.push(text.substr(lastOffset))
|
|
|
|
|
return code.join('')
|
|
|
|
|
}
|
|
|
|
|
function checkBrackets (url) {
|
|
|
|
|
var urlLength = url.length
|
|
|
|
|
var urlOpenBrackets = url.split('(').length - 1
|
|
|
|
|
var urlCloseBrackets = url.split(')').length - 1
|
|
|
|
|
while (urlCloseBrackets > urlOpenBrackets &&
|
|
|
|
|
url.charAt(urlLength - 1) === ')') {
|
|
|
|
|
url = url.substr(0, urlLength - 1)
|
|
|
|
|
urlCloseBrackets--
|
|
|
|
|
urlLength--
|
|
|
|
|
}
|
|
|
|
|
if (urlOpenBrackets > urlCloseBrackets) {
|
|
|
|
|
url = url.replace(/\)+$/, '')
|
|
|
|
|
}
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function replaceUrlEncodings(urlWithEncoded) {
|
|
|
|
|
return urlWithEncoded.replace(/(%[A-Z\d]{2})+/g, function (str) {
|
|
|
|
|
try {
|
|
|
|
|
return decodeURIComponent(str);
|
|
|
|
|
} catch (e) {
|
|
|
|
|
return str;
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function wrapPlainText(text, options = {}) {
|
|
|
|
|
if(emojiSupported) {
|
|
|
|
|
return text;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if(!text || !text.length) {
|
|
|
|
|
return '';
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
text = text.replace(/\ufe0f/g, '', text);
|
|
|
|
|
var match;
|
|
|
|
|
var raw = text;
|
|
|
|
|
var text = [],
|
|
|
|
|
emojiTitle;
|
|
|
|
|
while((match = raw.match(fullRegExp))) {
|
|
|
|
|
text.push(raw.substr(0, match.index))
|
|
|
|
|
if(match[8]) {
|
|
|
|
|
if((emojiCode = EmojiHelper.emojiMap[match[8]]) &&
|
|
|
|
|
(emojiTitle = emojiData[emojiCode][1][0])) {
|
|
|
|
|
text.push(':' + emojiTitle + ':');
|
|
|
|
|
} else {
|
|
|
|
|
text.push(match[0]);
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
text.push(match[0]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
raw = raw.substr(match.index + match[0].length);
|
|
|
|
|
}
|
|
|
|
|
text.push(raw);
|
|
|
|
|
return text.join('');
|
|
|
|
|
}
|
|
|
|
|
function wrapEmojiText(text) {
|
|
|
|
|
if(!text) return '';
|
|
|
|
|
|
|
|
|
|
let entities = parseEntities(text).filter(e => e._ == 'messageEntityEmoji');
|
|
|
|
|
return wrapRichText(text, {entities});
|
|
|
|
|
}
|
|
|
|
|
function wrapUrl (url, unsafe) {
|
|
|
|
|
if (!url.match(/^https?:\/\//i)) {
|
|
|
|
|
url = 'http://' + url
|
|
|
|
|
}
|
|
|
|
|
var tgMeMatch
|
|
|
|
|
var telescoPeMatch
|
|
|
|
|
if (unsafe == 2) {
|
|
|
|
|
url = 'tg://unsafe_url?url=' + encodeURIComponent(url)
|
|
|
|
|
}
|
|
|
|
|
else if ((tgMeMatch = url.match(/^https?:\/\/t(?:elegram)?\.me\/(.+)/))) {
|
|
|
|
|
var fullPath = tgMeMatch[1]
|
|
|
|
|
var path = fullPath.split('/')
|
|
|
|
|
switch (path[0]) {
|
|
|
|
|
case 'joinchat':
|
|
|
|
|
url = 'tg://join?invite=' + path[1]
|
|
|
|
|
break
|
|
|
|
|
case 'addstickers':
|
|
|
|
|
url = 'tg://addstickers?set=' + path[1]
|
|
|
|
|
break
|
|
|
|
|
default:
|
|
|
|
|
if (path[1] && path[1].match(/^\d+$/)) {
|
|
|
|
|
url = 'tg://resolve?domain=' + path[0] + '&post=' + path[1]
|
|
|
|
|
}
|
|
|
|
|
else if (path.length == 1) {
|
|
|
|
|
var domainQuery = path[0].split('?')
|
|
|
|
|
var domain = domainQuery[0]
|
|
|
|
|
var query = domainQuery[1]
|
|
|
|
|
if (domain == 'iv') {
|
|
|
|
|
var match = (query || '').match(/url=([^&=]+)/)
|
|
|
|
|
if (match) {
|
|
|
|
|
url = match[1]
|
|
|
|
|
try {
|
|
|
|
|
url = decodeURIComponent(url)
|
|
|
|
|
} catch (e) {}
|
|
|
|
|
return wrapUrl(url, unsafe)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
url = 'tg://resolve?domain=' + domain + (query ? '&' + query : '')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
else if ((telescoPeMatch = url.match(/^https?:\/\/telesco\.pe\/([^/?]+)\/(\d+)/))) {
|
|
|
|
|
url = 'tg://resolve?domain=' + telescoPeMatch[1] + '&post=' + telescoPeMatch[2]
|
|
|
|
|
}
|
|
|
|
|
else if (unsafe) {
|
|
|
|
|
url = 'tg://unsafe_url?url=' + encodeURIComponent(url)
|
|
|
|
|
}
|
|
|
|
|
return url
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
let RichTextProcessor = {
|
|
|
|
|
wrapRichText: wrapRichText,
|
|
|
|
|
wrapPlainText: wrapPlainText,
|
|
|
|
|
wrapDraftText: wrapDraftText,
|
|
|
|
|
wrapUrl: wrapUrl,
|
|
|
|
|
wrapEmojiText: wrapEmojiText,
|
|
|
|
|
parseEntities: parseEntities,
|
|
|
|
|
parseMarkdown: parseMarkdown,
|
|
|
|
|
parseEmojis: parseEmojis,
|
|
|
|
|
mergeEntities: mergeEntities,
|
|
|
|
|
getEmojiSpritesheetCoords: getEmojiSpritesheetCoords,
|
|
|
|
|
emojiSupported: emojiSupported
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
window.RichTextProcessor = RichTextProcessor;
|
|
|
|
|
|
|
|
|
|
export {RichTextProcessor};
|
|
|
|
|
|