// twister_formatpost.js
// 2013 Miguel Freitas
//
// Format JSON posts and DMs to HTML.
const _htmlFormatMsgAllowedUriSchemes = [ 'ed2k://' , 'ipfs://' , 'ipns://' , 'magnet:?' , 'xmpp:' ] ; // TODO it should be optional
var _htmlFormatMsgLinkTemplateExternal ;
var _htmlFormatMsgLinkTemplateUser ;
var _htmlFormatMsgLinkTemplateHashtag ;
$ ( function ( ) {
// we're setting it here for perfomance improvement purpose
// to not search and prepare it for for every post every time
_htmlFormatMsgLinkTemplateExternal = $ ( '#external-page-link-template' )
if ( _htmlFormatMsgLinkTemplateExternal . length ) {
_htmlFormatMsgLinkTemplateExternal = _htmlFormatMsgLinkTemplateExternal [ 0 ] . cloneNode ( ) ;
_htmlFormatMsgLinkTemplateExternal . removeAttribute ( 'id' ) ;
}
_htmlFormatMsgLinkTemplateUser = $ ( '#msg-user-link-template' )
if ( _htmlFormatMsgLinkTemplateUser . length ) {
_htmlFormatMsgLinkTemplateUser = _htmlFormatMsgLinkTemplateUser [ 0 ] . cloneNode ( ) ;
_htmlFormatMsgLinkTemplateUser . removeAttribute ( 'id' ) ;
}
_htmlFormatMsgLinkTemplateHashtag = $ ( '#hashtag-link-template' )
if ( _htmlFormatMsgLinkTemplateHashtag . length ) {
_htmlFormatMsgLinkTemplateHashtag = _htmlFormatMsgLinkTemplateHashtag [ 0 ] . cloneNode ( ) ;
_htmlFormatMsgLinkTemplateHashtag . removeAttribute ( 'id' ) ;
}
twister . tmpl . linkShortened = extractTemplate ( '#template-link-shortened' ) [ 0 ] ;
} ) ;
// format "userpost" to html element
// kind = "original"/"ancestor"/"descendant"
function postToElem ( post , kind , promoted , templatePost ) {
/ *
"userpost" :
{
"n" : username ,
"k" : seq number ,
"t" : "post" / "dm" / "rt"
"msg" : message ( post / rt )
"time" : unix utc
"height" : best height at user
"dm" : encrypted message ( dm ) - opt
"rt" : original userpost - opt
"sig_rt" : sig of rt - opt
"fav" : original userpost - opt
"sif_fav" : sig of fav - opt
"reply" : - opt
{
"n" : reference username
"k" : reference k
}
}
"sig_userpost" : signature by userpost . n
* /
/ * i f ( ! t w i s t e r . t w i s t s [ p o s t . u s e r p o s t . n ] )
twister . twists [ post . userpost . n ] = { } ;
else {
var i = post . sig _userpost ? post . userpost . k : - post . userpost . k ;
if ( twister . twists [ post . userpost . n ] [ i ] )
return twister . twists [ post . userpost . n ] [ i ] . elem . clone ( true ) ;
} * /
var username , k , time , msg , rt , content _to _rt , content _to _sigrt , retweeted _by ;
// Obtain data from userpost
var userpost = post . userpost ;
//TODO: favorites may have comment also...
if ( userpost . fav )
userpost = userpost . fav ;
if ( post . sig _wort )
userpost . sig _wort = post . sig _wort ;
if ( userpost . rt && typeof userpost . rt . msg === 'string' && userpost . rt . msg !== '' ) {
rt = userpost . rt ;
if ( userpost . msg ) {
username = userpost . n ;
k = userpost . k ;
time = userpost . time ;
msg = userpost . msg + ( userpost . msg2 || '' ) ;
content _to _rt = JSON . stringify ( userpost ) ;
content _to _sigrt = post . sig _userpost ;
} else {
username = rt . n ;
k = rt . k ;
time = rt . time ;
msg = rt . msg + ( rt . msg2 || '' ) ;
content _to _rt = JSON . stringify ( rt ) ;
content _to _sigrt = userpost . sig _rt ;
}
retweeted _by = userpost . n ;
} else {
username = userpost . n ;
k = userpost . k ;
time = userpost . time ;
msg = userpost . msg + ( userpost . msg2 || '' ) ;
content _to _rt = JSON . stringify ( userpost ) ;
content _to _sigrt = post . sig _userpost ;
}
if ( typeof msg !== 'string' )
msg = '' ;
if ( msg . length > $ . Options . MaxPostDisplayChars . val ) {
msg = msg . slice ( 0 , $ . Options . MaxPostDisplayChars . val ) + "\u2026" ;
}
if ( ! templatePost )
templatePost = twister . tmpl . post ;
// Now create the html elements
var elem = templatePost . clone ( true ) . appendTo ( twister . html . detached ) ;
elem . removeAttr ( 'id' )
. addClass ( kind )
. attr ( 'data-time' , time )
;
/ * v a r i = p o s t . s i g _ u s e r p o s t ? p o s t . u s e r p o s t . k : - p o s t . u s e r p o s t . k ;
twister . twists [ post . userpost . n ] [ i ] = {
data : post ,
elem : elem
} ;
var twistId = post . userpost . n + '/' + i ;
elem . attr ( 'data-twist-id' , twistId ) ; * /
if ( post . isNew )
elem . addClass ( 'new' ) ;
var postData = elem . find ( '.post-data' ) ;
postData . addClass ( kind )
. attr ( 'data-userpost' , JSON . stringify ( post ) )
. attr ( 'data-content_to_rt' , content _to _rt )
. attr ( 'data-content_to_sigrt' , content _to _sigrt )
. attr ( 'data-screen-name' , username )
. attr ( 'data-id' , k )
. attr ( 'data-lastk' , userpost . lastk )
. attr ( 'data-text' , msg )
;
if ( userpost . reply ) {
postData . attr ( 'data-replied-to-screen-name' , userpost . reply . n )
. attr ( 'data-replied-to-id' , userpost . reply . k )
. find ( '.post-expand' ) . text ( polyglot . t ( 'Show conversation' ) )
;
} else if ( userpost . rt && userpost . rt . reply ) {
postData . attr ( 'data-replied-to-screen-name' , userpost . rt . reply . n )
. attr ( 'data-replied-to-id' , userpost . rt . reply . k )
. find ( '.post-expand' ) . text ( polyglot . t ( 'Show conversation' ) )
;
}
setPostCommon ( elem , username , time ) ;
msg = fillElemWithTxt ( elem . find ( '.post-text' ) , msg ) ; // fillElemWithTxt() returns result of htmlFormatMsg(msg)
postData . attr ( 'data-text-mentions' , msg . mentions . join ( ) ) ; // FIXME no idea why do we need this attribute since we don't use it but use data-reply-to instead
if ( username !== defaultScreenName ) {
if ( msg . mentions . indexOf ( username ) === - 1 )
msg . mentions . splice ( 0 , 0 , username ) ;
}
for ( var i = msg . mentions . indexOf ( defaultScreenName ) ; i !== - 1 ; i = msg . mentions . indexOf ( defaultScreenName ) )
msg . mentions . splice ( i , 1 ) ;
if ( msg . mentions . length )
var replyTo = '@' + msg . mentions . join ( ' @' ) + ' ' ;
else
var replyTo = '' ;
var postTextArea = elem . find ( '.post-area-new textarea' ) ;
postTextArea . attr ( 'data-reply-to' , replyTo ) ;
if ( ! defaultScreenName )
postTextArea . attr ( 'placeholder' , polyglot . t ( 'You have to log in to post replies.' ) ) ;
else
postTextArea . attr ( 'placeholder' , polyglot . t ( 'reply_to' , { fullname : replyTo } ) + '...' ) ;
postData . attr ( 'data-reply-to' , replyTo ) ;
if ( typeof retweeted _by !== 'undefined' ) {
var postContext = elem . find ( '.post-context' ) ;
if ( userpost . msg ) {
setPostReference ( postContext , rt , userpost . sig _rt ) ;
} else {
postContext . append ( twister . tmpl . postRtBy . clone ( true ) ) . addClass ( 'post-rt-by' )
. find ( '.post-rt-sign .prep' ) . text ( polyglot . t ( 'post_rt_sign_prep' ) )
. siblings ( '.open-profile-modal' )
. attr ( 'href' , $ . MAL . userUrl ( retweeted _by ) ) . text ( '@' + retweeted _by )
;
postContext . find ( '.post-rt-time .prep' ) . text ( polyglot . t ( 'post_rt_time_prep' ) )
. siblings ( '.time' ) . text ( timeGmtToText ( post . userpost . time ) ) ;
// let's check original post and grab some possible RT
dhtget ( username , 'post' + k , 's' ,
function ( args , post ) {
if ( post && post . userpost . msg && post . userpost . rt ) {
var postContext = $ ( '<div class="post-context"></div>' ) ;
setPostReference ( postContext , post . userpost . rt , post . userpost . sig _rt ) ;
args . elem . find ( '.post-text' ) . after ( postContext ) ;
}
} , { elem : elem }
) ;
}
postContext . show ( ) ;
}
if ( typeof promoted !== 'undefined' && promoted ) {
elem . find ( '.post-propagate' ) . remove ( ) ;
postData . attr ( 'data-promoted' , 1 ) ;
postData . attr ( 'data-screen-name' , '!' + username ) ;
} else if ( ! post . sig _userpost ) { // .sig_userpost of promoted twists is empty so let's assume this one is promoted
elem . addClass ( 'promoted' )
. find ( '.post-text' ) . attr ( 'data-promoted' , polyglot . t ( 'promoted' ) ) ;
} else {
setPostInfoSent ( userpost . n , userpost . k , elem . find ( '.post-info-sent' ) ) ;
if ( $ . Options . filterLang . val !== 'disable' && $ . Options . filterLangSimulate . val ) {
// FIXME it's must be stuff from template actually
if ( typeof post . langFilter !== 'undefined' ) {
if ( typeof post . langFilter . prob [ 0 ] !== 'undefined' )
var mlm = ' // ' + polyglot . t ( 'Most possible language: this' ,
{ 'this' : '<em>' + post . langFilter . prob [ 0 ] . toString ( ) + '</em>' } ) ;
else
var mlm = '' ;
elem . append ( '<div class="langFilterSimData">'
+ polyglot . t ( 'This post is treated by language filter' ,
{ 'treated' : '<em>' + ( post . langFilter . pass ? polyglot . t ( 'passed' ) : polyglot . t ( 'blocked' ) ) + '</em>' } )
+ '</div>'
)
. append ( '<div class="langFilterSimData">'
+ polyglot . t ( 'Reason: this' , { 'this' : '<em>' + post . langFilter . reason + '</em>' } )
+ mlm + '</div>'
)
;
} else {
elem . append ( '<div class="langFilterSimData">'
+ polyglot . t ( 'This post is treated by language filter' ,
{ 'treated' : '<em>' + polyglot . t ( 'not analyzed' ) + '</em>' } )
+ '</div>'
)
;
}
}
}
return elem ; //.clone(true);
}
function setPostCommon ( elem , username , time ) {
var postInfoName = elem . find ( '.post-info-name' )
. text ( username ) . attr ( 'href' , $ . MAL . userUrl ( username ) ) ;
getFullname ( username , postInfoName ) ;
//elem.find('.post-info-tag').text("@" + username); // FIXME
getAvatar ( username , elem . find ( '.avatar' ) ) ;
elem . find ( '.post-info-time' )
. attr ( 'title' , timeSincePost ( time ) )
. find ( 'span:last' )
. text ( timeGmtToText ( time ) )
;
}
function setPostReference ( elem , rt , sig _rt ) {
elem . append ( twister . tmpl . postRtReference . clone ( true ) )
. find ( '.post-rt-reference' )
. attr ( 'data-screen-name' , rt . n )
. attr ( 'data-id' , rt . k )
. attr ( 'data-userpost' , JSON . stringify ( { userpost : rt , sig _userpost : sig _rt } ) )
. find ( '.post-text' ) . each ( function ( i , elem ) { fillElemWithTxt ( $ ( elem ) , rt . msg + ( rt . msg2 || '' ) ) ; } )
;
setPostCommon ( elem , rt . n , rt . time ) ;
}
function setPostInfoSent ( n , k , item ) {
if ( n === defaultScreenName && k >= 0 ) {
getPostMaxAvailability ( n , k ,
function ( args , count ) {
if ( count >= 2 ) { // assume 2 peers (me + 1) is enough for "sent"
args . item . text ( "\u2713" ) ; // check mark
} else {
args . item . text ( "\u231B" ) ; // hour glass
setTimeout ( setPostInfoSent , 30000 , n , k , item ) ;
}
} , { n : n , k : k , item : item } ) ;
}
}
// format dmdata (returned by getdirectmsgs) to display in conversation thread
function postToElemDM ( dmData , localUser , remoteUser ) {
var senderAlias = ( dmData . from && dmData . from . length && dmData . from . charCodeAt ( 0 ) )
? dmData . from : ( dmData . fromMe || dmData . from === localUser ? localUser : remoteUser ) ;
var elem = $ ( '#dm-chat-template' ) . clone ( true ) . appendTo ( twister . html . detached )
. removeAttr ( 'id' )
. addClass ( dmData . fromMe || dmData . from === localUser ? 'sent' : 'received' )
;
var elemName = elem . find ( '.post-info-name' )
. attr ( 'href' , $ . MAL . userUrl ( senderAlias ) ) ;
if ( senderAlias [ 0 ] === '*' )
getGroupChatName ( senderAlias , elemName ) ;
else
getFullname ( senderAlias , elemName ) ;
getAvatar ( senderAlias , elem . find ( '.post-photo' ) . find ( 'img' ) ) ;
elem . find ( '.post-info-time' )
. attr ( 'title' , timeSincePost ( dmData . time ) )
. find ( 'span:last' )
. text ( timeGmtToText ( dmData . time ) )
;
setPostInfoSent ( senderAlias , dmData . k , elem . find ( '.post-info-sent' ) ) ;
fillElemWithTxt ( elem . find ( '.post-text' ) , dmData . text ) ;
return elem ;
}
// convert message text to html, featuring @users and links formating.
function htmlFormatMsg ( msg , opt ) {
// TODO: add options for emotions; msg = $.emotions(msg);
function getSubStrStart ( str , startPoint , stopChars , isStopCharMustExist , stopCharsTrailing ) {
for ( var i = startPoint ; i > - 1 ; i -- ) {
if ( stopChars . indexOf ( str [ i ] ) > - 1 )
break ;
}
if ( i !== - 1 || ! isStopCharMustExist ) {
for ( i += 1 ; i < startPoint + 1 ; i ++ ) {
if ( stopCharsTrailing . indexOf ( str [ i ] ) === - 1 )
break ;
}
} else
i = startPoint + 1 ;
return i ;
}
function getSubStrEnd ( str , startPoint , stopChars , isStopCharMustExist , stopCharsTrailing ) {
for ( var i = startPoint ; i < str . length ; i ++ ) {
if ( stopChars . indexOf ( str [ i ] ) > - 1 )
break ;
}
if ( i !== str . length || ! isStopCharMustExist ) {
for ( i -= 1 ; i > startPoint - 1 ; i -- ) {
if ( stopCharsTrailing . indexOf ( str [ i ] ) === - 1 )
break ;
}
} else
i = startPoint - 1 ;
return i ;
}
function markout ( msg , markoutOpt , chr , tag ) {
if ( markoutOpt === 'ignore' )
return msg ;
function isWhiteSpacesBetween ( i , j ) {
j ++ ;
for ( i += 1 ; i < j ; i ++ ) {
if ( p [ i ] . w )
return true ;
}
return false ;
}
function kindOfL ( i ) {
if ( stopCharsMarkout . indexOf ( msg . str [ i ] ) > - 1 ) {
for ( var j = i - 1 ; j > - 1 ; j -- ) {
if ( msg . str [ j ] === chr ) {
return - 1 ;
} else if ( stopCharsMarkout . indexOf ( msg . str [ j ] ) === - 1 ) {
return whiteSpaces . indexOf ( msg . str [ j ] ) ;
}
}
} else if ( msg . str [ i ] === '<' ) {
for ( var j = i - 1 ; j > - 1 ; j -- ) {
if ( msg . str [ j ] === '>' ) {
if ( j === 0 ) {
return - 10 ;
} else {
if ( msg . str [ j - 1 ] === chr ) {
return - 1 ;
} else {
return kindOfL ( j - 1 ) ;
}
}
}
}
} else {
return whiteSpaces . indexOf ( msg . str [ i ] ) ;
}
return 0 ;
}
function kindOfR ( i ) {
if ( stopCharsMarkout . indexOf ( msg . str [ i ] ) > - 1 ) {
for ( var j = i + 1 ; j < msg . str . length ; j ++ ) {
if ( msg . str [ j ] === chr ) {
return - 1 ;
} else if ( stopCharsMarkout . indexOf ( msg . str [ j ] ) === - 1 ) {
return whiteSpaces . indexOf ( msg . str [ j ] ) ;
}
}
} else if ( msg . str [ i ] === '>' ) {
for ( var j = i + 1 ; j < msg . str . length ; j ++ ) {
if ( msg . str [ j ] === '<' ) {
if ( j === msg . str . length - 1 ) {
return - 10 ;
} else {
if ( msg . str [ j + 1 ] === chr ) {
return - 1 ;
} else {
return kindOfR ( j + 1 ) ;
}
}
}
}
} else {
return whiteSpaces . indexOf ( msg . str [ i ] ) ;
}
return 0 ;
}
var i , j , t , l , r , htmlEntityEncoded ;
var w = false ;
var p = [ ] ;
// collecting chars position data
for ( i = 0 ; i < msg . str . length ; i ++ ) {
if ( msg . str [ i ] === chr ) {
for ( j = i + 1 ; j < msg . str . length ; j ++ ) {
if ( msg . str [ j ] !== chr )
break ;
}
if ( i !== 0 ) {
l = kindOfL ( i - 1 ) ;
} else {
l = 0 ;
}
if ( j !== msg . str . length ) {
r = kindOfR ( j ) ;
} else {
r = 0 ;
}
if ( ( i === 0 && r < 0 ) || l === - 10 ) {
p . push ( { i : i , k : j - i , t : - 1 , w : w , a : - 1 , p : - 1 } ) ;
w = false ;
} else if ( ( j === msg . str . length && l < 0 ) || r === - 10 ) {
p . push ( { i : i , k : j - i , t : 1 , w : w , a : - 1 , p : - 1 } ) ;
w = false ;
} else {
if ( l > - 1 ) {
if ( r > - 1 ) {
if ( j - i > 2 ) {
l = p . push ( { i : i , k : j - i - 1 , t : - 1 , w : w , a : - 1 , p : - 1 } ) - 1 ;
p [ l ] . a = p . push ( { i : j - 1 , k : 1 , t : 1 , w : false , a : l , p : - 1 } ) - 1 ;
}
t = 10 ;
} else
t = - 1 ;
} else {
if ( r > - 1 )
t = 1 ;
else
t = 0 ;
}
if ( t !== 10 )
p . push ( { i : i , k : j - i , t : t , w : w , a : - 1 , p : - 1 } ) ;
w = false ;
}
i = j - 1 ;
} else if ( ! w && whiteSpaces . indexOf ( msg . str [ i ] ) > - 1 ) {
w = true ;
}
}
// calculating dependencies
for ( i = 0 ; i < p . length ; i ++ ) {
if ( p [ i ] . t < 1 && p [ i ] . a === - 1 ) {
t = i ;
for ( j = i + 1 ; j < p . length ; j ++ ) {
if ( p [ i ] . t === 0 && isWhiteSpacesBetween ( i , j ) ) {
i = j - 1 ;
break ;
} else if ( p [ j ] . t < 1 && p [ j ] . a === - 1 ) {
p [ t ] . p = j ;
t = j ;
} else if ( p [ j ] . t === 1 && p [ j ] . a === - 1 ) {
p [ i ] . a = j ;
p [ j ] . a = i ;
i = j ;
break ;
}
}
}
}
for ( i = 0 ; i < p . length ; i ++ ) {
if ( p [ i ] . t === - 1 && p [ i ] . a === - 1 ) {
for ( j = p [ i ] . p ; j > - 1 ; j = p [ j ] . p ) {
if ( isWhiteSpacesBetween ( i , j ) ) {
i = j - 1 ;
break ;
} else if ( p [ j ] . t === 0
&& ! ( p [ j ] . p > - 1 && p [ p [ j ] . p ] . t === 0 && ! isWhiteSpacesBetween ( j , p [ j ] . p ) ) ) {
p [ j ] . a = i ;
p [ i ] . a = j ;
i = j ;
break ;
}
}
}
}
// changing the string
if ( markoutOpt === 'apply' ) {
t = '</' + tag + '>' ;
tag = '<' + tag + '>' ;
} else { // markoutOpt === 'clear' so we're clearing markup
t = '' ;
tag = '' ;
}
for ( i = 0 ; i < p . length ; i ++ ) {
if ( p [ i ] . a > - 1 ) {
if ( p [ i ] . t === - 1 || ( p [ i ] . t === 0 && p [ i ] . a > i ) ) {
if ( p [ i ] . k > 1 )
msg . htmlEntities . push ( tag + Array ( p [ i ] . k ) . join ( chr ) ) ;
else
msg . htmlEntities . push ( tag ) ;
} else if ( p [ i ] . t === 1 || ( p [ i ] . t === 0 && p [ i ] . a < i ) ) {
if ( p [ i ] . k > 1 )
msg . htmlEntities . push ( Array ( p [ i ] . k ) . join ( chr ) + t ) ;
else
msg . htmlEntities . push ( t ) ;
}
htmlEntityEncoded = '>' + ( msg . htmlEntities . length - 1 ) . toString ( ) + '<' ;
msg . str = msg . str . slice ( 0 , p [ i ] . i ) + htmlEntityEncoded + msg . str . slice ( p [ i ] . i + p [ i ] . k ) ;
l = htmlEntityEncoded . length - p [ i ] . k ;
for ( j = i + 1 ; j < p . length ; j ++ )
p [ j ] . i += l ;
}
}
return msg ;
}
function newHtmlEntityLink ( template , urlTarget , urlName ) {
template . href = urlTarget ;
template . innerHTML = urlName ; // .innerHTML instead .text to allow markup inside [] of [url name](target)
return template . outerHTML ;
}
function msgAddHtmlEntity ( msg , strSliceLeft , strSliceRigth , htmlEntity ) {
msg . htmlEntities . push ( htmlEntity ) ;
var htmlEntityEncoded = '>' + ( msg . htmlEntities . length - 1 ) . toString ( ) + '<' ;
msg . str = msg . str . slice ( 0 , strSliceLeft ) + htmlEntityEncoded + msg . str . slice ( strSliceRigth ) ;
msg . i = strSliceLeft + htmlEntityEncoded . length - 1 ;
return msg ;
}
function applyHtml ( msg ) {
var t ;
for ( var i = 0 ; i < msg . str . length - 2 ; i ++ ) {
if ( msg . str [ i ] === '>' ) {
for ( var j = i + 2 ; j < msg . str . length ; j ++ ) {
if ( msg . str [ j ] === '<' )
break ;
}
t = msg . htmlEntities [ parseInt ( msg . str . slice ( i + 1 , j ) ) ] ;
msg . str = msg . str . slice ( 0 , i ) + t + msg . str . slice ( j + 1 ) ;
i = i + t . length - 1 ;
}
}
return msg . str ;
}
if ( ! msg )
return { html : '' , mentions : [ ] } ;
if ( opt && opt . markout )
var markoutOpt = opt . markout ;
else
var markoutOpt = $ . Options . postsMarkout . val ;
var mentionsChars = 'abcdefghijklmnopqrstuvwxyz_0123456789' ;
var stopCharsTrailing = '/\\*~_-`.,:;?!%\'"[](){}^|«»…\u201C\u201D\u2026\u2014\u4E00\u3002\uFF0C\uFF1A\uFF1F\uFF01\u3010\u3011\u2047\u2048\u2049' ;
var stopCharsTrailingUrl = stopCharsTrailing . slice ( 1 ) ;
var whiteSpaces = ' \f\n\r\t\v \u00A0\u1680\u180E\u2000 \u2001\u2002 \u2003\u2004\u2005\u2006 \u2007\u2008 \u2009\u200A\u2028\u2029 \u202F\u205F\u3000' ;
var whiteSpacesUrl = '\'\"' + whiteSpaces ;
var stopCharsLeft = '<' + whiteSpaces ;
var stopCharsRight = '>' + whiteSpaces ;
var stopCharsRightHashtags = '>/\\.,:;?!%\'"[](){}^|«»…\u201C\u201D\u2026\u2014\u4E00\u3002\uFF0C\uFF1A\uFF1F\uFF01\u3010\u3011\u2047\u2048\u2049' // same as stopCharsTrailing but without '*~_-`' plus '>'
+ whiteSpaces ;
var stopCharsRightHashtagsBase64 = stopCharsRightHashtags . replace ( '/' , '' ) . replace ( '+' , '' ) // exclude valid base64 chars used in shortened urls
var stopCharsMarkout = '/\\*~_-`.,:;?!%+=&\'"[](){}^|«»…\u201C\u201D\u2026\u2014\u4E00\u3002\uFF0C\uFF1A\uFF1F\uFF01\u3010\u3011\u2047\u2048\u2049' ;
var i , j , k , str ;
var mentions = [ ] ;
msg = { str : escapeHtmlEntities ( msg ) , htmlEntities : [ ] } ;
// markout is not applied for chars inside of ``; to escape ` use backslash
if ( markoutOpt === 'apply' )
msg . str = msg . str . replace ( /`(?:[^`\\]|\\.)*`/g , function ( s ) {
msg . htmlEntities . push ( '<samp>' + s . slice ( 1 , - 1 ) . replace ( /\\`/g , '`' )
. replace ( /&(?!lt;|gt;)/g , '&' ) + '</samp>' ) ;
return '>' + ( msg . htmlEntities . length - 1 ) . toString ( ) + '<' ;
} ) ;
else if ( markoutOpt === 'clear' )
msg . str = msg . str . replace ( /`((?:[^`\\]|\\.)*)`/g , function ( s ) {
return s . slice ( 1 , - 1 ) . replace ( /\\`/g , '`' ) ;
} ) ;
// handling links
for ( i = 0 ; i < msg . str . length - 7 ; i ++ ) {
if ( msg . str . slice ( i , i + 2 ) === '](' && markoutOpt !== 'ignore' ) {
// FIXME there can be text with [] inside [] or links with () we need to handle it too
j = getSubStrStart ( msg . str , i - 2 , '[' , true , '' ) ;
if ( j < i - 1 ) {
k = getSubStrEnd ( msg . str , i + 3 , ')' , true , whiteSpaces ) ;
if ( k > i + 2 ) {
var linkName = msg . str . slice ( j , i ) ; // name of possiible link
for ( i += 2 ; i < k ; i ++ ) {
if ( whiteSpacesUrl . indexOf ( msg . str [ i ] ) === - 1 ) // drop whitespaces and ' and " // apostrophes and quotes to prevent injection of js events
break ;
}
if ( i < k ) {
// following check is NOT for real protection (we have blocking CSP rule instead), it's just to aware people
if ( isUriSuspicious ( msg . str . slice ( i , k + 1 ) ) ) {
msg = msgAddHtmlEntity ( msg , j - 1 , getSubStrEnd ( msg . str , k + 1 , ')' , true , '' ) + 2 ,
'…<br><b><i>' + polyglot . t ( 'busted_oh' ) + '</i> '
+ polyglot . t ( 'busted_avowal' ) + ':</b><br><samp>['
+ linkName
. replace ( /&(?!lt;|gt;)/g , '&' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' )
+ ']('
+ msg . str . slice ( i , k + 1 )
. replace ( /&(?!lt;|gt;)/g , '&' )
. replace ( /"/g , '"' )
. replace ( /'/g , ''' )
+ ')</samp><br>…<br>'
) ;
} else {
var x = getSubStrEnd ( msg . str , i + 1 , whiteSpacesUrl , false , '' ) ;
if ( x < k ) // use only first word as href target, others drop silently
k = x ;
linkName = applyHtml ( // we're handling markup inside [] of []()
markout ( markout ( markout ( markout (
{ str : linkName , htmlEntities : msg . htmlEntities } ,
markoutOpt , '*' , 'b' ) , // bold
markoutOpt , '~' , 'i' ) , // italic
markoutOpt , '_' , 'u' ) , // underlined
markoutOpt , '-' , 's' ) // striketrough
)
. replace ( /&(?!lt;|gt;)/g , '&' ) ;
if ( markoutOpt === 'apply' ) {
if ( msg . str . slice ( i , i + 6 ) . toLowerCase ( ) === 'twist:' && msg . str [ i + 17 ] === '='
&& getSubStrStart ( msg . str , i + 16 , stopCharsRightHashtagsBase64 , false , '' ) === i + 6 )
msg = msgAddHtmlEntity ( msg , j - 1 , getSubStrEnd ( msg . str , k + 1 , ')' , true , '' ) + 2 ,
newHtmlEntityLink ( twister . tmpl . linkShortened ,
msg . str . slice ( i , i + 18 ) , linkName )
) ;
else
msg = msgAddHtmlEntity ( msg , j - 1 , getSubStrEnd ( msg . str , k + 1 , ')' , true , '' ) + 2 ,
newHtmlEntityLink ( _htmlFormatMsgLinkTemplateExternal ,
proxyURL ( msg . str . slice ( i , k + 1 ) ) , linkName )
) ;
} else { // markoutOpt === 'clear' so we're clearing markup
str = msg . str . slice ( i , k + 1 ) ;
msg = msgAddHtmlEntity ( msg , j - 1 , getSubStrEnd ( msg . str , k + 1 , ')' , true , '' ) + 2 ,
linkName ) ;
// here we put link target as plain text to handle it usual way (search http[s]:// and so on)
i = msg . i + 1
msg . str = msg . str . slice ( 0 , i ) + ' ' + str + msg . str . slice ( i ) ;
/ * a l t e r n a t i v e l y w e c o u l d s e t u p i t a s l i n k i t s e l f b u t I s u p p o s e y o u d o n ' t w a n t i t
msg = msgAddHtmlEntity ( msg , msg . i + 1 , msg . i + 1 ,
' ' + newHtmlEntityLink ( _htmlFormatMsgLinkTemplateExternal ,
proxyURL ( str ) , str )
) ; * /
}
}
i = msg . i + 1 ;
}
}
}
} else if ( msg . str . slice ( i , i + 4 ) . toLowerCase ( ) === 'http' ) {
if ( msg . str . slice ( i + 4 , i + 7 ) === '://' && stopCharsRight . indexOf ( msg . str [ i + 7 ] ) === - 1 ) {
j = getSubStrEnd ( msg . str , i + 7 , stopCharsRight , false , stopCharsTrailingUrl ) ;
if ( j > i + 6 ) {
str = msg . str . slice ( i , j + 1 ) ;
msg = msgAddHtmlEntity ( msg , i , i + str . length ,
newHtmlEntityLink ( _htmlFormatMsgLinkTemplateExternal ,
proxyURL ( str ) , str )
) ;
i = msg . i ;
}
} else if ( msg . str . slice ( i + 4 , i + 8 ) . toLowerCase ( ) === 's://' && stopCharsRight . indexOf ( msg . str [ i + 8 ] ) === - 1 ) {
j = getSubStrEnd ( msg . str , i + 8 , stopCharsRight , false , stopCharsTrailingUrl ) ;
if ( j > i + 7 ) {
str = msg . str . slice ( i , j + 1 ) ;
msg = msgAddHtmlEntity ( msg , i , i + str . length ,
newHtmlEntityLink ( _htmlFormatMsgLinkTemplateExternal ,
proxyURL ( str ) , str )
) ;
i = msg . i ;
}
}
} else if ( msg . str . slice ( i , i + 6 ) . toLowerCase ( ) === 'twist:' && msg . str [ i + 17 ] === '='
&& getSubStrStart ( msg . str , i + 16 , stopCharsRightHashtagsBase64 , false , '' ) === i + 6 ) {
str = msg . str . slice ( i , i + 18 ) ;
msg = msgAddHtmlEntity ( msg , i , i + str . length ,
newHtmlEntityLink ( twister . tmpl . linkShortened , str , str ) ) ;
i = msg . i ;
} else {
for ( k = 0 ; k < _htmlFormatMsgAllowedUriSchemes . length ; k ++ ) {
let l = _htmlFormatMsgAllowedUriSchemes [ k ] . length ;
if ( _htmlFormatMsgAllowedUriSchemes [ k ] === msg . str . slice ( i , i + l ) . toLowerCase ( )
&& stopCharsRight . indexOf ( msg . str [ i + l ] ) === - 1 ) {
j = getSubStrEnd ( msg . str , i + l , stopCharsRight , false , stopCharsTrailingUrl ) ;
if ( j > i + l - 1 ) {
str = msg . str . slice ( i , j + 1 ) ;
msg = msgAddHtmlEntity ( msg , i , i + str . length ,
newHtmlEntityLink ( _htmlFormatMsgLinkTemplateExternal ,
str , str )
) ;
i = msg . i ;
break ;
}
}
}
}
}
// handling mails
for ( i = 1 ; i < msg . str . length - 1 ; i ++ ) {
if ( msg . str [ i ] === '@' && stopCharsLeft . indexOf ( msg . str [ i - 1 ] ) === - 1
&& stopCharsTrailing . indexOf ( msg . str [ i - 1 ] ) === - 1 && stopCharsRight . indexOf ( msg . str [ i + 1 ] ) === - 1 ) {
j = getSubStrStart ( msg . str , i - 1 , stopCharsLeft , false , stopCharsTrailing ) ;
if ( j < i ) {
k = getSubStrEnd ( msg . str , i + 1 , stopCharsRight , false , stopCharsTrailing ) ;
if ( k > i ) {
str = msg . str . slice ( j , k + 1 ) ;
msg = msgAddHtmlEntity ( msg , j , j + str . length ,
newHtmlEntityLink ( _htmlFormatMsgLinkTemplateExternal ,
'mailto:' + str . toLowerCase ( ) , str )
) ;
i = msg . i ;
}
}
}
}
// handling mentions
for ( i = 0 ; i < msg . str . length - 1 ; i ++ ) {
if ( msg . str [ i ] === '@' && mentionsChars . indexOf ( msg . str [ i + 1 ] . toLowerCase ( ) ) > - 1 ) {
for ( j = i + 2 ; j < msg . str . length ; j ++ ) {
if ( mentionsChars . indexOf ( msg . str [ j ] . toLowerCase ( ) ) === - 1 )
break ;
}
str = msg . str . slice ( i + 1 , j ) . toLowerCase ( ) ;
mentions . push ( str ) ;
msg = msgAddHtmlEntity ( msg , i , i + str . length + 1 ,
newHtmlEntityLink ( _htmlFormatMsgLinkTemplateUser ,
$ . MAL . userUrl ( str ) , '@' + str )
) ;
i = msg . i ;
}
}
// handling hashtags
for ( i = 0 ; i < msg . str . length - 1 ; i ++ ) {
if ( msg . str [ i ] === '#' && msg . str [ i + 1 ] !== '#' && stopCharsRight . indexOf ( msg . str [ i + 1 ] ) === - 1 ) {
j = getSubStrEnd ( msg . str , i + 1 , stopCharsRightHashtags , false , stopCharsTrailing ) ;
if ( j > i ) {
str = msg . str . slice ( i + 1 , j + 1 ) ;
msg = msgAddHtmlEntity ( msg , i , i + str . length + 1 ,
newHtmlEntityLink ( _htmlFormatMsgLinkTemplateHashtag ,
$ . MAL . hashtagUrl ( encodeURIComponent ( str . toLowerCase ( ) ) ) , '#' + str . replace ( /&/g , '&' ) )
) ;
i = msg . i ;
}
}
}
// handling text style markup
msg = markout ( markout ( markout ( markout ( msg ,
markoutOpt , '*' , 'b' ) , // bold
markoutOpt , '~' , 'i' ) , // italic
markoutOpt , '_' , 'u' ) , // underlined
markoutOpt , '-' , 's' ) // striketrough
;
// handling splitted posts numbering and escaping ampersands, qoutes and apostrophes
msg . str = msg . str
. replace ( /\(\d{1,2}\/\d{1,2}\)$/ , function ( str ) {
msg . htmlEntities . push ( '<span class="splited-post-counter">' + str + '</span>' ) ; // FIXME
return '>' + ( msg . htmlEntities . length - 1 ) . toString ( ) + '<' ;
} )
. replace ( /&(?!lt;|gt;)/g , '&' ) // FIXME in many cases there is no need to escape ampersand in HTML 5
. replace ( /"/g , '"' )
. replace ( /'/g , ''' )
;
// applying html entities to msg.str and converting msg to string back
msg = applyHtml ( msg ) ;
// handling linebreaks
if ( $ . Options . displayLineFeeds . val === 'enable' )
msg = msg . replace ( /\n/g , '<br />' ) ;
return { html : msg , mentions : mentions } ;
}
function isUriSuspicious ( req ) {
var colonPos = req . search ( /:|%3A/gi ) ;
if ( colonPos === 0 )
return true ;
var hashPos = req . search ( /#|%23/g ) ;
if ( colonPos === - 1 )
if ( hashPos > - 1 )
return req = req . slice ( hashPos + 1 ) ,
( req . search ( /^(?:hashtag|profile|conversation|mentions|favs|directmessages|groupmessages|newusers|followers|following|whotofollow|groupmessages\+newgroup|groupmessages\+joingroup\/uri\-shortener)\b/ ) > - 1 ) ? false : true ;
else
return false ; //(req.search(/^\s*@[A-Za-z0-9]+\/\d/g) === 0) ? false : true;
if ( hashPos > - 1 && hashPos < colonPos )
return true ;
return req = req . slice ( req . search ( /\S/g ) , colonPos ) ,
req . search ( /[^A-Za-z0-9\+\.\-]/ ) > - 1 || req . search ( /(?:script|data)$/i ) > - 1 ;
}
function proxyURL ( url ) {
var proxyOpt = $ . Options . useProxy . val ;
if ( proxyOpt !== 'disable' && ! $ . Options . useProxyForImgOnly . val
&& url [ 0 ] !== '/' && url . indexOf ( location . origin ) !== 0 ) {
// proxy alternatives may be added to options page FIXME currently not; and we need more fresh proxies
if ( proxyOpt === 'ssl-proxy-my-addr' ) {
url = 'https://ssl-proxy.my-addr.org/myaddrproxy.php/' +
url . substring ( 0 , url . indexOf ( ':' ) ) + url . substr ( url . indexOf ( '/' ) + 1 ) ;
} else if ( proxyOpt === 'anonymouse' )
url = 'http://anonymouse.org/cgi-bin/anon-www.cgi/' + url ;
}
return url ;
}
function escapeHtmlEntities ( str ) {
return str
//.replace(/&/g, '&') we do it not here
. replace ( /</g , '<' )
. replace ( />/g , '>' )
//.replace(/"/g, '"')
//.replace(/'/g, ''')
;
}
function reverseHtmlEntities ( str ) {
return str
. replace ( /</g , '<' )
. replace ( />/g , '>' )
. replace ( /"/g , '"' )
. replace ( /'/g , "'" )
. replace ( /&/g , '&' ) ;
}
function setPostImagePreview ( elem , links ) {
if ( $ . Options . displayPreview . val === 'enable' ) {
var previewContainer = elem . find ( '.preview-container' ) ;
// was the preview added before...
if ( ! previewContainer . children ( ) . length ) {
// is there any links to images in the post?
for ( var i = 0 ; i < links . length ; i ++ ) {
if ( /^[^?]+\.(?:jpe?g|gif|png|webp)$/i . test ( links [ i ] . href ) ) {
var url = proxyURL ( links [ i ] . href ) ;
previewContainer . append ( $ ( '<img src="' + url + '" class="image-preview" />' ) ) ;
}
}
}
if ( previewContainer . children ( ) . length )
previewContainer . show ( ) ;
else
previewContainer . hide ( ) ;
}
}