2021-04-08 17:52:31 +04:00
/ *
* https : //github.com/morethanwords/tweb
* Copyright ( C ) 2019 - 2021 Eduard Kuzmenko
* https : //github.com/morethanwords/tweb/blob/master/LICENSE
*
* Originally from :
* https : //github.com/zhukov/webogram
* Copyright ( C ) 2014 Igor Zhukov < igor.beatle @ gmail.com >
* https : //github.com/zhukov/webogram/blob/master/LICENSE
* /
2020-06-13 11:19:39 +03:00
import Config from './config' ;
2021-04-08 17:52:31 +04:00
import emojiRegExp from '../vendor/emoji/regex' ;
2021-05-25 14:00:47 +03:00
import { encodeEmoji , toCodePoints } from '../vendor/emoji' ;
2020-10-16 23:03:30 +03:00
import { MessageEntity } from '../layer' ;
2020-11-07 05:48:07 +02:00
import { encodeEntities } from '../helpers/string' ;
2020-11-27 16:27:27 +02:00
import { isSafari } from '../helpers/userAgent' ;
2021-02-13 19:32:10 +04:00
import { MOUNT_CLASS_TO } from '../config/debug' ;
2021-06-25 18:16:16 +03:00
import IS_EMOJI_SUPPORTED from '../helpers/emojiSupport' ;
2020-08-29 18:10:04 +03:00
2020-10-16 23:03:30 +03:00
const EmojiHelper = {
2020-08-29 18:10:04 +03:00
emojiMap : ( code : string ) = > { return code ; } ,
shortcuts : [ ] as any ,
emojis : [ ] as any
2020-06-13 11:19:39 +03:00
} ;
2020-10-16 23:03:30 +03:00
const emojiData = Config . Emoji ;
2020-06-13 11:19:39 +03:00
2020-10-16 23:03:30 +03:00
const alphaCharsRegExp = 'a-z' +
2020-06-13 11:19:39 +03:00
'\\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)
2020-10-16 23:03:30 +03:00
const alphaNumericRegExp = '0-9\_' + alphaCharsRegExp ;
const domainAddChars = '\u00b7' ;
2020-06-13 11:19:39 +03:00
// Based on Regular Expression for URL validation by Diego Perini
2021-05-14 07:23:17 +04:00
const urlAlphanumericRegExpPart = '[' + alphaCharsRegExp + '0-9]' ;
const urlProtocolRegExpPart = '((?:https?|ftp)://|mailto:)?' ;
const urlRegExp = urlProtocolRegExpPart +
2020-06-13 11:19:39 +03:00
// user:pass authentication
2021-05-14 07:23:17 +04:00
'(?:' + urlAlphanumericRegExpPart + '{1,64}(?::' + urlAlphanumericRegExpPart + '{0,64})?@)?' +
2020-06-13 11:19:39 +03:00
'(?:' +
// 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
2021-05-14 07:23:17 +04:00
urlAlphanumericRegExpPart + '[' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}' +
2020-06-13 11:19:39 +03:00
// domain name
2021-05-14 07:23:17 +04:00
'(?:\\.' + urlAlphanumericRegExpPart + '[' + alphaCharsRegExp + domainAddChars + '0-9\-]{0,64}){0,10}' +
2020-06-13 11:19:39 +03:00
// TLD identifier
'(?:\\.(xn--[0-9a-z]{2,16}|[' + alphaCharsRegExp + ']{2,24}))' +
')' +
// port number
'(?::\\d{2,5})?' +
// resource path
2020-08-29 18:10:04 +03:00
'(?:/(?:\\S{0,255}[^\\s.;,(\\[\\]{}<>"\'])?)?' ;
2021-05-14 07:23:17 +04:00
const urlProtocolRegExp = new RegExp ( '^' + urlProtocolRegExpPart . slice ( 0 , - 1 ) , 'i' ) ;
const urlAnyProtocolRegExp = /^((?:.+?):\/\/|mailto:)/ ;
2020-10-16 23:03:30 +03:00
const usernameRegExp = '[a-zA-Z\\d_]{5,32}' ;
const botCommandRegExp = '\\/([a-zA-Z\\d_]{1,32})(?:@(' + usernameRegExp + '))?(\\b|$)' ;
2021-05-14 07:23:17 +04:00
const fullRegExp = new RegExp ( '(^| )(@)(' + usernameRegExp + ')|(' + urlRegExp + ')|(\\n)|(' + emojiRegExp + ')|(^|[\\s\\(\\]])(#[' + alphaNumericRegExp + ']{2,64})|(^|\\s)' + botCommandRegExp , 'i' ) ;
2020-10-16 23:03:30 +03:00
const 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,}))$/ ;
2020-11-14 04:27:13 +02:00
//const markdownTestRegExp = /[`_*@~]/;
2020-11-22 09:56:20 +02:00
const markdownRegExp = /(^|\s|\n)(````?)([\s\S]+?)(````?)([\s\n\.,:?!;]|$)|(^|\s|\x01)(`|~~|\*\*|__|_-_)([^\n]+?)\7([\x01\s\.,:?!;]|$)|@(\d+)\s*\((.+?)\)|(\[(.+?)\]\((.+?)\))/m ;
2020-10-16 23:03:30 +03:00
const siteHashtags : { [ siteName : string ] : string } = {
2020-06-13 11:19:39 +03:00
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}'
2020-08-29 18:10:04 +03:00
} ;
2020-10-16 23:03:30 +03:00
const siteMentions : { [ siteName : string ] : string } = {
2020-06-13 11:19:39 +03:00
Telegram : '#/im?p=%40{1}' ,
Twitter : 'https://twitter.com/{1}' ,
Instagram : 'https://instagram.com/{1}/' ,
GitHub : 'https://github.com/{1}'
2020-08-29 18:10:04 +03:00
} ;
2021-05-14 07:23:17 +04:00
const markdownEntities : { [ markdown : string ] : MessageEntity [ '_' ] } = {
2020-06-13 11:19:39 +03:00
'`' : 'messageEntityCode' ,
2020-11-14 04:27:13 +02:00
'``' : 'messageEntityPre' ,
2020-06-13 11:19:39 +03:00
'**' : 'messageEntityBold' ,
2020-11-14 04:27:13 +02:00
'__' : 'messageEntityItalic' ,
2020-11-22 09:56:20 +02:00
'~~' : 'messageEntityStrike' ,
'_-_' : 'messageEntityUnderline'
2020-08-29 18:10:04 +03:00
} ;
2020-06-13 11:19:39 +03:00
2021-05-15 21:03:51 +04:00
const passConflictingEntities : Set < MessageEntity [ '_' ] > = new Set ( [
'messageEntityEmoji' ,
2021-05-25 13:45:35 +03:00
'messageEntityLinebreak' ,
'messageEntityCaret'
2021-05-15 21:03:51 +04:00
] ) ;
2021-05-14 07:23:17 +04:00
for ( let i in markdownEntities ) {
passConflictingEntities . add ( markdownEntities [ i ] ) ;
}
2020-10-16 23:03:30 +03:00
namespace RichTextProcessor {
2021-06-25 18:16:16 +03:00
export const emojiSupported = IS_EMOJI_SUPPORTED ;
2020-09-01 22:15:50 +03:00
2020-10-16 23:03:30 +03:00
export function getEmojiSpritesheetCoords ( emojiCode : string ) {
2021-05-28 16:17:46 +03:00
let unified = encodeEmoji ( emojiCode ) . replace ( /-?fe0f/g , '' ) ;
2020-10-16 23:03:30 +03:00
2021-05-28 16:17:46 +03:00
/ * i f ( u n i f i e d = = = ' 1 f 4 4 1 - 2 0 0 d - 1 f 5 e 8 ' ) {
2021-05-25 13:45:35 +03:00
//unified = '1f441-fe0f-200d-1f5e8-fe0f';
unified = '1f441-fe0f-200d-1f5e8' ;
2021-05-28 16:17:46 +03:00
} * /
2020-10-16 23:03:30 +03:00
2021-05-28 16:17:46 +03:00
if ( ! emojiData . hasOwnProperty ( unified )
// && !emojiData.hasOwnProperty(unified.replace(/-?fe0f$/, ''))
) {
2020-10-16 23:03:30 +03:00
//console.error('lol', unified);
return null ;
}
2021-05-28 16:17:46 +03:00
return unified ;
2020-09-01 22:15:50 +03:00
}
2020-10-16 23:03:30 +03:00
export function parseEntities ( text : string ) {
2021-05-14 07:23:17 +04:00
let match : any ;
let raw = text ;
2020-10-16 23:03:30 +03:00
const entities : MessageEntity [ ] = [ ] ;
let matchIndex ;
2021-05-14 07:23:17 +04:00
let rawOffset = 0 ;
2020-10-16 23:03:30 +03:00
// var start = tsNow()
2021-05-25 13:45:35 +03:00
fullRegExp . lastIndex = 0 ;
2020-10-16 23:03:30 +03:00
while ( ( match = raw . match ( fullRegExp ) ) ) {
matchIndex = rawOffset + match . index ;
//console.log('parseEntities match:', match);
if ( match [ 3 ] ) { // mentions
2020-06-13 11:19:39 +03:00
entities . push ( {
2020-10-16 23:03:30 +03:00
_ : 'messageEntityMention' ,
offset : matchIndex + match [ 1 ] . length ,
length : match [ 2 ] . length + match [ 3 ] . length
2020-06-13 11:19:39 +03:00
} ) ;
2020-10-16 23:03:30 +03:00
} else if ( match [ 4 ] ) {
if ( emailRegExp . test ( match [ 4 ] ) ) { // email
entities . push ( {
_ : 'messageEntityEmail' ,
offset : matchIndex ,
length : match [ 4 ] . length
} ) ;
} else {
2021-05-14 07:23:17 +04:00
let url : string ;
let protocol = match [ 5 ] ;
const tld = match [ 6 ] ;
// let excluded = '';
2020-10-16 23:03:30 +03:00
if ( tld ) { // URL
if ( ! protocol && ( tld . substr ( 0 , 4 ) === 'xn--' || Config . TLD . indexOf ( tld . toLowerCase ( ) ) !== - 1 ) ) {
protocol = 'http://' ;
2020-06-13 11:19:39 +03:00
}
2020-10-16 23:03:30 +03:00
if ( protocol ) {
2021-05-14 07:23:17 +04:00
const balanced = checkBrackets ( match [ 4 ] ) ;
if ( balanced . length !== match [ 4 ] . length ) {
// excluded = match[4].substring(balanced.length);
2020-10-16 23:03:30 +03:00
match [ 4 ] = balanced ;
}
url = ( match [ 5 ] ? '' : protocol ) + match [ 4 ] ;
}
} else { // IP address
url = ( match [ 5 ] ? '' : 'http://' ) + match [ 4 ] ;
}
2021-05-14 07:23:17 +04:00
if ( url ) {
2020-10-16 23:03:30 +03:00
entities . push ( {
_ : 'messageEntityUrl' ,
offset : matchIndex ,
length : match [ 4 ] . length
} ) ;
2020-06-13 11:19:39 +03:00
}
}
2020-10-16 23:03:30 +03:00
} else if ( match [ 7 ] ) { // New line
entities . push ( {
_ : 'messageEntityLinebreak' ,
offset : matchIndex ,
length : 1
} ) ;
} else if ( match [ 8 ] ) { // Emoji
//console.log('hit', match[8]);
2021-05-14 07:23:17 +04:00
const emojiCoords = getEmojiSpritesheetCoords ( match [ 8 ] ) ;
2020-10-16 23:03:30 +03:00
if ( emojiCoords ) {
2020-06-13 11:19:39 +03:00
entities . push ( {
2020-10-16 23:03:30 +03:00
_ : 'messageEntityEmoji' ,
2020-06-13 11:19:39 +03:00
offset : matchIndex ,
2020-10-16 23:03:30 +03:00
length : match [ 8 ] . length ,
unicode : emojiCoords
2020-06-13 11:19:39 +03:00
} ) ;
}
2020-10-16 23:03:30 +03:00
} else if ( match [ 11 ] ) { // Hashtag
2020-06-13 11:19:39 +03:00
entities . push ( {
2020-10-16 23:03:30 +03:00
_ : 'messageEntityHashtag' ,
offset : matchIndex + ( match [ 10 ] ? match [ 10 ] . length : 0 ) ,
length : match [ 11 ] . length
} ) ;
2021-05-28 20:01:06 +03:00
} else if ( match [ 13 ] ) { // Bot command
2020-10-16 23:03:30 +03:00
entities . push ( {
_ : 'messageEntityBotCommand' ,
2021-06-18 20:01:30 +03:00
offset : matchIndex + ( match [ 11 ] ? match [ 11 ] . length : 0 ) + ( match [ 12 ] ? match [ 12 ] . length : 0 ) ,
2021-05-28 20:01:06 +03:00
length : 1 + match [ 13 ] . length + ( match [ 14 ] ? 1 + match [ 14 ] . length : 0 ) ,
unsafe : true
2020-06-13 11:19:39 +03:00
} ) ;
}
2020-10-16 23:03:30 +03:00
raw = raw . substr ( match . index + match [ 0 ] . length ) ;
rawOffset += match . index + match [ 0 ] . length ;
2020-06-13 11:19:39 +03:00
}
2020-10-16 23:03:30 +03:00
// if (entities.length) {
// console.log('parse entities', text, entities.slice())
// }
return entities ;
2020-06-13 11:19:39 +03:00
}
2020-10-16 23:03:30 +03:00
/ * e x p o r t f u n c t i o n p a r s e E m o j i s ( t e x t : s t r i n g ) {
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
} )
} * /
2020-08-29 18:10:04 +03:00
2021-05-18 17:17:54 +03:00
export function parseMarkdown ( text : string , currentEntities : MessageEntity [ ] , noTrim? : boolean ) : string {
2020-11-14 04:27:13 +02:00
/ * i f ( ! m a r k d o w n T e s t R e g E x p . t e s t ( t e x t ) ) {
2020-10-16 23:03:30 +03:00
return noTrim ? text : text.trim ( ) ;
2020-11-14 04:27:13 +02:00
} * /
2020-12-24 00:03:34 +02:00
const entities : MessageEntity [ ] = [ ] ;
2021-04-29 20:05:42 +04:00
let pushedEntity = false ;
2021-05-14 07:23:17 +04:00
const pushEntity = ( entity : MessageEntity ) = > ! findConflictingEntity ( currentEntities , entity ) ? ( entities . push ( entity ) , pushedEntity = true ) : pushedEntity = false ;
2021-04-29 20:05:42 +04:00
2020-11-22 09:56:20 +02:00
let raw = text ;
let match ;
let newText : any = [ ] ;
let rawOffset = 0 ;
2020-11-14 04:27:13 +02:00
while ( match = raw . match ( markdownRegExp ) ) {
2020-11-22 09:56:20 +02:00
const matchIndex = rawOffset + match . index ;
2020-11-14 04:27:13 +02:00
newText . push ( raw . substr ( 0 , match . index ) ) ;
2021-04-29 20:05:42 +04:00
let text = ( match [ 3 ] || match [ 8 ] || match [ 11 ] || match [ 13 ] ) ;
2020-11-14 04:27:13 +02:00
rawOffset -= text . length ;
2020-12-15 17:51:11 +02:00
//text = text.replace(/^\s+|\s+$/g, '');
2020-11-14 04:27:13 +02:00
rawOffset += text . length ;
2021-04-29 20:05:42 +04:00
let entity : MessageEntity ;
pushedEntity = false ;
2020-11-14 04:27:13 +02:00
if ( text . match ( /^`*$/ ) ) {
newText . push ( match [ 0 ] ) ;
} else if ( match [ 3 ] ) { // pre
2021-04-29 20:05:42 +04:00
entity = {
2020-10-16 23:03:30 +03:00
_ : 'messageEntityPre' ,
language : '' ,
offset : matchIndex + match [ 1 ] . length ,
length : text.length
2021-04-29 20:05:42 +04:00
} ;
2020-11-14 04:27:13 +02:00
2021-04-29 20:05:42 +04:00
if ( pushEntity ( entity ) ) {
if ( match [ 5 ] === '\n' ) {
match [ 5 ] = '' ;
rawOffset -= 1 ;
}
newText . push ( match [ 1 ] + text + match [ 5 ] ) ;
rawOffset -= match [ 2 ] . length + match [ 4 ] . length ;
}
2020-11-14 04:27:13 +02:00
} else if ( match [ 7 ] ) { // code|italic|bold
2021-02-03 06:36:59 +02:00
const isSOH = match [ 6 ] === '\x01' ;
2020-11-22 09:56:20 +02:00
2021-04-29 20:05:42 +04:00
entity = {
2021-05-14 07:23:17 +04:00
_ : markdownEntities [ match [ 7 ] ] as ( MessageEntity . messageEntityBold | MessageEntity . messageEntityCode | MessageEntity . messageEntityItalic ) [ '_' ] ,
2020-11-22 09:56:20 +02:00
//offset: matchIndex + match[6].length,
offset : matchIndex + ( isSOH ? 0 : match [ 6 ] . length ) ,
2020-10-16 23:03:30 +03:00
length : text.length
2021-04-29 20:05:42 +04:00
} ;
2020-11-14 04:27:13 +02:00
2021-04-29 20:05:42 +04:00
if ( pushEntity ( entity ) ) {
if ( ! isSOH ) {
newText . push ( match [ 6 ] + text + match [ 9 ] ) ;
} else {
newText . push ( text ) ;
}
rawOffset -= match [ 7 ] . length * 2 + ( isSOH ? 2 : 0 ) ;
}
2020-11-14 04:27:13 +02:00
} else if ( match [ 11 ] ) { // custom mention
2021-04-29 20:05:42 +04:00
entity = {
2020-10-16 23:03:30 +03:00
_ : 'messageEntityMentionName' ,
user_id : + match [ 10 ] ,
offset : matchIndex ,
length : text.length
2021-04-29 20:05:42 +04:00
} ;
if ( pushEntity ( entity ) ) {
newText . push ( text ) ;
rawOffset -= match [ 0 ] . length - text . length ;
}
2020-11-14 04:27:13 +02:00
} else if ( match [ 12 ] ) { // text url
2021-04-29 20:05:42 +04:00
entity = {
2020-11-14 04:27:13 +02:00
_ : 'messageEntityTextUrl' ,
2021-04-29 20:05:42 +04:00
url : match [ 14 ] ,
2020-11-14 04:27:13 +02:00
offset : matchIndex ,
length : text.length
2021-04-29 20:05:42 +04:00
} ;
if ( pushEntity ( entity ) ) {
newText . push ( text ) ;
2020-11-14 04:27:13 +02:00
2021-04-29 20:05:42 +04:00
rawOffset -= match [ 12 ] . length - text . length ;
}
}
if ( ! pushedEntity ) {
newText . push ( match [ 0 ] ) ;
2020-10-16 23:03:30 +03:00
}
2020-11-14 04:27:13 +02:00
raw = raw . substr ( match . index + match [ 0 ] . length ) ;
rawOffset += match . index + match [ 0 ] . length ;
2020-06-13 11:19:39 +03:00
}
2020-11-14 04:27:13 +02:00
newText . push ( raw ) ;
newText = newText . join ( '' ) ;
if ( ! newText . replace ( /\s+/g , '' ) . length ) {
newText = text ;
entities . splice ( 0 , entities . length ) ;
2020-06-13 11:19:39 +03:00
}
2020-11-14 04:27:13 +02:00
if ( ! entities . length && ! noTrim ) {
newText = newText . trim ( ) ;
2020-10-16 23:03:30 +03:00
}
2020-11-14 04:27:13 +02:00
2020-12-24 00:03:34 +02:00
mergeEntities ( currentEntities , entities ) ;
combineSameEntities ( currentEntities ) ;
2020-11-14 04:27:13 +02:00
return newText ;
2020-10-16 23:03:30 +03:00
}
2020-08-29 18:10:04 +03:00
2021-05-14 07:23:17 +04:00
export function findConflictingEntity ( currentEntities : MessageEntity [ ] , newEntity : MessageEntity ) {
2021-04-29 20:05:42 +04:00
return currentEntities . find ( currentEntity = > {
2021-05-14 07:23:17 +04:00
const isConflictingTypes = newEntity . _ === currentEntity . _ ||
( ! passConflictingEntities . has ( newEntity . _ ) && ! passConflictingEntities . has ( currentEntity . _ ) ) ;
if ( ! isConflictingTypes ) {
return false ;
}
const isConflictingOffset = newEntity . offset >= currentEntity . offset &&
2021-04-29 20:05:42 +04:00
( newEntity . length + newEntity . offset ) <= ( currentEntity . length + currentEntity . offset ) ;
2021-05-14 07:23:17 +04:00
return isConflictingOffset ;
2021-04-29 20:05:42 +04:00
} ) ;
}
2020-12-19 03:07:24 +02:00
export function mergeEntities ( currentEntities : MessageEntity [ ] , newEntities : MessageEntity [ ] ) {
2021-04-29 20:05:42 +04:00
const filtered = newEntities . filter ( e = > {
2021-05-14 07:23:17 +04:00
return ! findConflictingEntity ( currentEntities , e ) ;
2021-04-29 20:05:42 +04:00
} ) ;
2020-12-16 00:13:54 +02:00
currentEntities . push ( . . . filtered ) ;
currentEntities . sort ( ( a , b ) = > a . offset - b . offset ) ;
return currentEntities ;
2020-10-16 23:03:30 +03:00
}
2020-08-29 18:10:04 +03:00
2020-12-24 00:03:34 +02:00
export function combineSameEntities ( entities : MessageEntity [ ] ) {
//entities = entities.slice();
for ( let i = 0 ; i < entities . length ; ++ i ) {
const entity = entities [ i ] ;
let nextEntityIdx = - 1 ;
do {
nextEntityIdx = entities . findIndex ( ( e , _i ) = > _i !== i && e . _ === entity . _ && ( e . offset - entity . length ) === entity . offset ) ;
if ( nextEntityIdx !== - 1 ) {
const nextEntity = entities [ nextEntityIdx ] ;
entity . length += nextEntity . length ;
entities . splice ( nextEntityIdx , 1 ) ;
}
} while ( nextEntityIdx !== - 1 ) ;
2020-06-13 11:19:39 +03:00
}
2020-12-24 00:03:34 +02:00
//return entities;
}
2020-08-29 18:10:04 +03:00
2020-10-16 23:03:30 +03:00
export function wrapRichText ( text : string , options : Partial < {
entities : MessageEntity [ ] ,
contextSite : string ,
highlightUsername : string ,
2020-10-21 02:25:36 +03:00
noLinks : true ,
noLinebreaks : true ,
noCommands : true ,
2020-11-27 16:27:27 +02:00
wrappingDraft : true ,
2021-06-14 20:33:04 +03:00
//mustWrapEmoji: boolean,
2020-10-16 23:03:30 +03:00
fromBot : boolean ,
2020-10-21 02:25:36 +03:00
noTextFormat : true ,
2020-11-14 04:27:13 +02:00
passEntities : Partial < {
2021-05-29 16:06:55 +03:00
[ _ in MessageEntity [ '_' ] ] : boolean
2020-11-14 04:27:13 +02:00
} > ,
2021-06-14 20:33:04 +03:00
contextHashtag? : string ,
2020-12-16 00:13:54 +02:00
} > = { } ) {
2021-05-28 20:01:06 +03:00
if ( ! text ) {
2020-12-16 00:13:54 +02:00
return '' ;
}
const lol : {
part : string ,
offset : number
} [ ] = [ ] ;
const entities = options . entities || parseEntities ( text ) ;
const passEntities : typeof options . passEntities = options . passEntities || { } ;
const contextSite = options . contextSite || 'Telegram' ;
2021-02-04 02:30:23 +02:00
const contextExternal = contextSite !== 'Telegram' ;
2020-12-16 00:13:54 +02:00
const insertPart = ( entity : MessageEntity , startPart : string , endPart? : string ) = > {
lol . push ( { part : startPart , offset : entity.offset } ) ;
if ( endPart ) {
lol . unshift ( { part : endPart , offset : entity.offset + entity . length } ) ;
}
} ;
2021-04-24 17:35:00 +04:00
for ( let i = 0 , length = entities . length ; i < length ; ++ i ) {
const entity = entities [ i ] ;
2020-12-16 00:13:54 +02:00
switch ( entity . _ ) {
case 'messageEntityBold' : {
if ( ! options . noTextFormat ) {
if ( options . wrappingDraft ) {
insertPart ( entity , '<span style="font-weight: bold;">' , '</span>' ) ;
} else {
insertPart ( entity , '<strong>' , '</strong>' ) ;
}
}
break ;
}
case 'messageEntityItalic' : {
if ( ! options . noTextFormat ) {
if ( options . wrappingDraft ) {
insertPart ( entity , '<span style="font-style: italic;">' , '</span>' ) ;
} else {
insertPart ( entity , '<em>' , '</em>' ) ;
}
}
break ;
}
case 'messageEntityStrike' : {
if ( options . wrappingDraft ) {
const styleName = isSafari ? 'text-decoration' : 'text-decoration-line' ;
insertPart ( entity , ` <span style=" ${ styleName } : line-through;"> ` , '</span>' ) ;
} else {
insertPart ( entity , '<del>' , '</del>' ) ;
}
break ;
}
case 'messageEntityUnderline' : {
if ( options . wrappingDraft ) {
const styleName = isSafari ? 'text-decoration' : 'text-decoration-line' ;
insertPart ( entity , ` <span style=" ${ styleName } : underline;"> ` , '</span>' ) ;
} else {
insertPart ( entity , '<u>' , '</u>' ) ;
}
break ;
}
case 'messageEntityCode' : {
if ( options . wrappingDraft ) {
insertPart ( entity , '<span style="font-family: monospace;">' , '</span>' ) ;
} else {
insertPart ( entity , '<code>' , '</code>' ) ;
}
break ;
}
case 'messageEntityPre' : {
if ( ! options . noTextFormat ) {
insertPart ( entity , ` <pre><code ${ entity . language ? ' class="language-' + encodeEntities ( entity . language ) + '"' : '' } > ` , '</code></pre>' ) ;
}
break ;
}
case 'messageEntityHighlight' : {
2021-01-23 00:12:57 +02:00
insertPart ( entity , '<i class="text-highlight">' , '</i>' ) ;
2020-12-16 00:13:54 +02:00
break ;
}
case 'messageEntityBotCommand' : {
2021-05-29 16:06:55 +03:00
// if(!(options.noLinks || options.noCommands || contextExternal)/* && !entity.unsafe */) {
if ( ! options . noLinks && passEntities [ entity . _ ] ) {
2020-12-16 00:13:54 +02:00
const entityText = text . substr ( entity . offset , entity . length ) ;
let command = entityText . substr ( 1 ) ;
let bot : string | boolean ;
let atPos : number ;
2021-02-04 02:30:23 +02:00
if ( ( atPos = command . indexOf ( '@' ) ) !== - 1 ) {
2020-12-16 00:13:54 +02:00
bot = command . substr ( atPos + 1 ) ;
command = command . substr ( 0 , atPos ) ;
} else {
bot = options . fromBot ;
}
2021-05-29 16:06:55 +03:00
insertPart ( entity , ` <a href=" ${ encodeEntities ( 'tg://bot_command?command=' + encodeURIComponent ( command ) + ( bot ? '&bot=' + encodeURIComponent ( bot ) : '' ) ) } " ${ contextExternal ? '' : 'onclick="execBotCommand(this)"' } > ` , ` </a> ` ) ;
2020-12-16 00:13:54 +02:00
}
break ;
}
case 'messageEntityEmoji' : {
2021-05-28 16:17:46 +03:00
//if(!(options.wrappingDraft && emojiSupported)) { // * fix safari emoji
if ( ! emojiSupported ) { // no wrapping needed
// if(emojiSupported) { // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера
// insertPart(entity, '<span class="emoji">', '</span>');
// } else {
2020-12-16 00:13:54 +02:00
insertPart ( entity , ` <img src="assets/img/emoji/ ${ entity . unicode } .png" alt=" ` , ` " class="emoji"> ` ) ;
2021-05-28 16:17:46 +03:00
// }
2021-06-14 20:33:04 +03:00
} / * else if ( options . mustWrapEmoji ) {
insertPart ( entity , '<span class="emoji">' , '</span>' ) ;
} * /
2021-01-03 19:59:13 +04:00
/ * i f ( ! e m o j i S u p p o r t e d ) {
insertPart ( entity , ` <img src="assets/img/emoji/ ${ entity . unicode } .png" alt=" ` , ` " class="emoji"> ` ) ;
} * /
2020-12-16 00:13:54 +02:00
break ;
}
2021-05-25 13:45:35 +03:00
case 'messageEntityCaret' : {
insertPart ( entity , '<span class="composer-sel"></span>' ) ;
break ;
}
2020-12-16 00:13:54 +02:00
/ * c a s e ' m e s s a g e E n t i t y L i n e b r e a k ' : {
if ( options . noLinebreaks ) {
insertPart ( entity , ' ' ) ;
} else {
insertPart ( entity , '<br/>' ) ;
}
break ;
} * /
case 'messageEntityUrl' :
case 'messageEntityTextUrl' : {
if ( ! ( options . noLinks && ! passEntities [ entity . _ ] ) ) {
const entityText = text . substr ( entity . offset , entity . length ) ;
2021-05-14 07:23:17 +04:00
// let inner: string;
2021-06-29 16:17:10 +03:00
let url : string = ( entity as MessageEntity . messageEntityTextUrl ) . url || entityText ;
2021-04-24 17:35:00 +04:00
let masked = false ;
2021-06-29 16:17:10 +03:00
let onclick : string ;
const wrapped = wrapUrl ( url , true ) ;
url = wrapped . url ;
onclick = wrapped . onclick ;
2021-04-24 17:35:00 +04:00
2021-06-29 16:17:10 +03:00
if ( entity . _ === 'messageEntityTextUrl' ) {
2021-04-24 17:35:00 +04:00
const nextEntity = entities [ i + 1 ] ;
if ( nextEntity ? . _ === 'messageEntityUrl' &&
nextEntity . length === entity . length &&
nextEntity . offset === entity . offset ) {
i ++ ;
2021-05-14 07:23:17 +04:00
}
if ( url !== entityText ) {
2021-04-24 17:35:00 +04:00
masked = true ;
}
2020-12-16 00:13:54 +02:00
} else {
//inner = encodeEntities(replaceUrlEncodings(entityText));
}
const currentContext = url [ 0 ] === '#' ;
2021-06-29 16:17:10 +03:00
if ( ! onclick && masked && ! currentContext ) {
onclick = 'showMaskedAlert' ;
}
2020-12-16 00:13:54 +02:00
2021-05-06 19:08:16 -07:00
const href = ( currentContext || typeof electronHelpers === 'undefined' )
? encodeEntities ( url )
: ` javascript:electronHelpers.openExternal(' ${ encodeEntities ( url ) } '); ` ;
const target = ( currentContext || typeof electronHelpers !== 'undefined' )
? '' : ' target="_blank" rel="noopener noreferrer"' ;
2021-06-29 16:17:10 +03:00
insertPart ( entity , ` <a class="anchor-url" href=" ${ href } " ${ target } ${ onclick ? ` onclick=" ${ onclick } (this)" ` : '' } > ` , '</a>' ) ;
2020-12-16 00:13:54 +02:00
}
break ;
}
case 'messageEntityEmail' : {
if ( ! options . noLinks ) {
const entityText = text . substr ( entity . offset , entity . length ) ;
insertPart ( entity , ` <a href=" ${ encodeEntities ( 'mailto:' + entityText ) } " target="_blank" rel="noopener noreferrer"> ` , '</a>' ) ;
}
break ;
}
case 'messageEntityHashtag' : {
const contextUrl = ! options . noLinks && siteHashtags [ contextSite ] ;
if ( contextUrl ) {
const entityText = text . substr ( entity . offset , entity . length ) ;
const hashtag = entityText . substr ( 1 ) ;
2021-05-28 20:01:06 +03:00
insertPart ( entity , ` <a class="anchor-hashtag" href=" ${ contextUrl . replace ( '{1}' , encodeURIComponent ( hashtag ) ) } " ${ contextExternal ? ' target="_blank" rel="noopener noreferrer"' : ' onclick="searchByHashtag(this)"' } > ` , '</a>' ) ;
2020-12-16 00:13:54 +02:00
}
break ;
}
case 'messageEntityMentionName' : {
2021-06-01 05:06:41 +03:00
if ( ! ( options . noLinks && ! passEntities [ entity . _ ] ) ) {
insertPart ( entity , ` <a href="#/im?p= ${ encodeURIComponent ( entity . user_id ) } " class="follow" data-follow=" ${ entity . user_id } "> ` , '</a>' ) ;
2020-12-16 00:13:54 +02:00
}
break ;
}
case 'messageEntityMention' : {
const contextUrl = ! options . noLinks && siteMentions [ contextSite ] ;
if ( contextUrl ) {
const entityText = text . substr ( entity . offset , entity . length ) ;
const username = entityText . substr ( 1 ) ;
insertPart ( entity , ` <a class="mention" href=" ${ contextUrl . replace ( '{1}' , encodeURIComponent ( username ) ) } " ${ contextExternal ? ' target="_blank" rel="noopener noreferrer"' : '' } > ` , '</a>' ) ;
}
break ;
}
}
}
lol . sort ( ( a , b ) = > a . offset - b . offset ) ;
2021-07-02 01:39:50 +03:00
const arr : string [ ] = [ ] ;
2020-12-16 00:13:54 +02:00
let usedLength = 0 ;
for ( const { part , offset } of lol ) {
if ( offset > usedLength ) {
2021-07-02 01:39:50 +03:00
arr . push ( encodeEntities ( text . slice ( usedLength , offset ) ) ) ;
2020-12-16 00:13:54 +02:00
usedLength = offset ;
}
2021-07-02 01:39:50 +03:00
arr . push ( part ) ;
2020-12-16 00:13:54 +02:00
}
if ( usedLength < text . length ) {
2021-07-02 01:39:50 +03:00
arr . push ( encodeEntities ( text . slice ( usedLength ) ) ) ;
2020-12-16 00:13:54 +02:00
}
2021-07-02 01:39:50 +03:00
return arr . join ( '' ) ;
2020-12-16 00:13:54 +02:00
}
2021-05-28 16:17:46 +03:00
export function fixEmoji ( text : string , entities? : MessageEntity [ ] ) {
/ * i f ( ! e m o j i S u p p o r t e d ) {
return text ;
} * /
// '$`\ufe0f'
text = text . replace ( /[\u2640\u2642\u2764](?!\ufe0f)/g , ( match , offset , string ) = > {
if ( entities ) {
const length = match . length ;
offset += length ;
entities . forEach ( entity = > {
const end = entity . offset + entity . length ;
if ( end === offset ) { // current entity
entity . length += length ;
} else if ( end > offset ) {
entity . offset += length ;
}
} ) ;
}
// console.log([match, offset, string]);
return match + '\ufe0f' ;
} ) ;
return text ;
}
2020-11-14 04:27:13 +02:00
export function wrapDraftText ( text : string , options : Partial < {
entities : MessageEntity [ ]
} > = { } ) {
2020-11-24 06:13:16 +02:00
if ( ! text ) {
return '' ;
}
2020-11-14 04:27:13 +02:00
return wrapRichText ( text , {
2021-01-25 21:06:44 +02:00
entities : options.entities ,
2020-11-14 04:27:13 +02:00
noLinks : true ,
2020-11-27 16:27:27 +02:00
wrappingDraft : true ,
2020-11-14 04:27:13 +02:00
passEntities : {
2021-06-01 05:06:41 +03:00
messageEntityTextUrl : true ,
messageEntityMentionName : true
2020-11-14 04:27:13 +02:00
}
} ) ;
2020-06-13 11:19:39 +03:00
}
2020-10-16 23:03:30 +03:00
export function checkBrackets ( url : string ) {
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 ;
}
export function replaceUrlEncodings ( urlWithEncoded : string ) {
return urlWithEncoded . replace ( /(%[A-Z\d]{2})+/g , ( str ) = > {
try {
return decodeURIComponent ( str ) ;
} catch ( e ) {
return str ;
}
} ) ;
}
export function wrapPlainText ( text : any ) {
if ( emojiSupported ) {
return text ;
}
if ( ! text || ! text . length ) {
return '' ;
}
text = text . replace ( /\ufe0f/g , '' , text ) ;
var match ;
var raw = text ;
var text : any = [ ] ,
emojiTitle ;
2021-05-25 13:45:35 +03:00
fullRegExp . lastIndex = 0 ;
2020-10-16 23:03:30 +03:00
while ( ( match = raw . match ( fullRegExp ) ) ) {
text . push ( raw . substr ( 0 , match . index ) )
if ( match [ 8 ] ) {
// @ts-ignore
const emojiCode = EmojiHelper . emojiMap [ match [ 8 ] ] ;
if ( emojiCode &&
// @ts-ignore
( emojiTitle = emojiData [ emojiCode ] [ 1 ] [ 0 ] ) ) {
text . push ( ':' + emojiTitle + ':' ) ;
} else {
text . push ( match [ 0 ] ) ;
}
2020-06-13 11:19:39 +03:00
} else {
text . push ( match [ 0 ] ) ;
}
2020-10-16 23:03:30 +03:00
raw = raw . substr ( match . index + match [ 0 ] . length ) ;
2020-06-13 11:19:39 +03:00
}
2020-10-16 23:03:30 +03:00
text . push ( raw ) ;
return text . join ( '' ) ;
2020-06-13 11:19:39 +03:00
}
2020-10-16 23:03:30 +03:00
export function wrapEmojiText ( text : string ) {
if ( ! text ) return '' ;
2021-02-03 06:36:59 +02:00
let entities = parseEntities ( text ) . filter ( e = > e . _ === 'messageEntityEmoji' ) ;
2020-10-16 23:03:30 +03:00
return wrapRichText ( text , { entities } ) ;
2020-06-13 11:19:39 +03:00
}
2020-08-29 18:10:04 +03:00
2021-06-29 16:17:10 +03:00
export function wrapUrl ( url : string , unsafe? : number | boolean ) : { url : string , onclick : string } {
2021-05-14 07:23:17 +04:00
if ( ! matchUrlProtocol ( url ) ) {
2020-10-16 23:22:41 +03:00
url = 'https://' + url ;
2020-10-16 23:03:30 +03:00
}
2021-02-03 06:15:28 +02:00
let tgMeMatch ;
let telescoPeMatch ;
2021-06-29 16:17:10 +03:00
let onclick : string ;
2021-02-03 06:36:59 +02:00
/ * i f ( u n s a f e = = = 2 ) {
2020-10-16 23:03:30 +03:00
url = 'tg://unsafe_url?url=' + encodeURIComponent ( url ) ;
2021-05-14 07:23:17 +04:00
} else * /if((tgMeMatch = url.match(/ ^ ( ? : https ? : \ / \ / ) ? t ( ? : elegram ) ? \ . me \ / ( . + ) / ) ) ) {
2021-02-03 06:15:28 +02:00
const fullPath = tgMeMatch [ 1 ] ;
const path = fullPath . split ( '/' ) ;
2020-10-16 23:03:30 +03:00
switch ( path [ 0 ] ) {
case 'joinchat' :
2021-06-29 16:17:10 +03:00
case 'addstickers' :
onclick = path [ 0 ] ;
break ;
/ * c a s e ' j o i n c h a t ' :
onclick = 'joinchat' ;
2020-10-16 23:03:30 +03:00
url = 'tg://join?invite=' + path [ 1 ] ;
break ;
case 'addstickers' :
2021-06-29 16:17:10 +03:00
onclick = 'addstickers' ;
2020-10-16 23:03:30 +03:00
url = 'tg://addstickers?set=' + path [ 1 ] ;
2021-06-29 16:17:10 +03:00
break ; * /
2020-10-16 23:03:30 +03:00
default :
2021-02-03 06:15:28 +02:00
if ( path [ 1 ] && path [ 1 ] . match ( /^\d+$/ ) ) { // https://t.me/.+/[0-9]+ (channel w/ username)
if ( path [ 0 ] === 'c' && path [ 2 ] ) { // https://t.me/c/111111111/111 (channel w/o username)
url = '#/im?p=' + path [ 1 ] + '&post=' + path [ 2 ] ;
} else { // https://t.me/durov/151 (channel w/ username)
url = siteMentions [ 'Telegram' ] . replace ( '{1}' , path [ 0 ] + '&post=' + path [ 1 ] ) ;
}
} else if ( path . length === 1 ) {
const domainQuery = path [ 0 ] . split ( '?' ) ;
const domain = domainQuery [ 0 ] ;
const query = domainQuery [ 1 ] ;
if ( domain === 'iv' ) {
const match = ( query || '' ) . match ( /url=([^&=]+)/ ) ;
2020-10-16 23:03:30 +03:00
if ( match ) {
url = match [ 1 ] ;
try {
url = decodeURIComponent ( url ) ;
} catch ( e ) { }
return wrapUrl ( url , unsafe ) ;
}
2020-06-13 11:19:39 +03:00
}
2020-10-16 23:03:30 +03:00
2020-12-16 00:13:54 +02:00
url = siteMentions [ 'Telegram' ] . replace ( '{1}' , domain + ( query ? '&' + query : '' ) ) ;
//url = 'tg://resolve?domain=' + domain + (query ? '&' + query : '');
2020-06-13 11:19:39 +03:00
}
2021-02-03 06:15:28 +02:00
break ;
2020-10-16 23:03:30 +03:00
}
2021-05-14 07:23:17 +04:00
} else if ( ( telescoPeMatch = url . match ( /^(?:https?:\/\/)?telesco\.pe\/([^/?]+)\/(\d+)/ ) ) ) {
2020-10-16 23:03:30 +03:00
url = 'tg://resolve?domain=' + telescoPeMatch [ 1 ] + '&post=' + telescoPeMatch [ 2 ] ;
2020-10-17 00:08:16 +03:00
} / * else if ( unsafe ) {
url = 'tg://unsafe_url?url=' + encodeURIComponent ( url ) ;
} * /
2020-10-16 23:03:30 +03:00
2021-06-29 16:17:10 +03:00
return { url , onclick } ;
2020-10-16 23:03:30 +03:00
}
2021-05-14 07:23:17 +04:00
export function matchUrlProtocol ( text : string ) {
return ! text ? null : text . match ( urlAnyProtocolRegExp ) ;
}
2020-10-16 23:03:30 +03:00
export function matchUrl ( text : string ) {
2020-12-18 05:07:32 +02:00
return ! text ? null : text . match ( urlRegExp ) ;
2020-06-13 11:19:39 +03:00
}
2020-10-29 18:26:08 +02:00
2021-02-27 15:15:34 +04:00
export function matchEmail ( text : string ) {
return ! text ? null : text . match ( emailRegExp ) ;
}
2020-11-19 01:51:39 +02:00
export function getAbbreviation ( str : string , onlyFirst = false ) {
const splitted = str . trim ( ) . split ( ' ' ) ;
if ( ! splitted [ 0 ] ) return '' ;
const first = [ . . . splitted [ 0 ] ] [ 0 ] ;
2021-02-03 06:36:59 +02:00
if ( onlyFirst || splitted . length === 1 ) return wrapEmojiText ( first ) ;
2020-11-19 01:51:39 +02:00
const last = [ . . . splitted [ splitted . length - 1 ] ] [ 0 ] ;
return wrapEmojiText ( first + last ) ;
2020-10-29 18:26:08 +02:00
}
2021-03-16 19:18:51 +04:00
export function isUsernameValid ( username : string ) {
return ( ( username . length >= 5 && username . length <= 32 ) || ! username . length ) && /^[a-zA-Z0-9_]*$/ . test ( username ) ;
}
2021-05-25 14:00:47 +03:00
export function getEmojiEntityFromEmoji ( emoji : string ) : MessageEntity . messageEntityEmoji {
return {
_ : 'messageEntityEmoji' ,
offset : 0 ,
length : emoji.length ,
unicode : toCodePoints ( emoji ) . join ( '-' ) . replace ( /-?fe0f/g , '' )
} ;
}
export function wrapSingleEmoji ( emoji : string ) {
return wrapRichText ( emoji , {
entities : [ getEmojiEntityFromEmoji ( emoji ) ]
} ) ;
}
2020-10-03 20:45:56 +03:00
}
2021-03-27 19:34:08 +04:00
MOUNT_CLASS_TO . RichTextProcessor = RichTextProcessor ;
2020-06-13 11:19:39 +03:00
export { RichTextProcessor } ;
2020-10-16 23:03:30 +03:00
export default RichTextProcessor ;
2020-06-13 11:19:39 +03:00