Telegram Web K with changes to work inside I2P
https://web.telegram.i2p/
You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
455 lines
14 KiB
455 lines
14 KiB
/* |
|
* https://github.com/morethanwords/tweb |
|
* Copyright (C) 2019-2021 Eduard Kuzmenko |
|
* https://github.com/morethanwords/tweb/blob/master/LICENSE |
|
*/ |
|
|
|
import { SITE_HASHTAGS } from "."; |
|
import { EmojiVersions } from "../../config/emoji"; |
|
import IS_EMOJI_SUPPORTED from "../../environment/emojiSupport"; |
|
import { IS_SAFARI } from "../../environment/userAgent"; |
|
import buildURLHash from "../../helpers/buildURLHash"; |
|
import copy from "../../helpers/object/copy"; |
|
import encodeEntities from "../../helpers/string/encodeEntities"; |
|
import { MessageEntity } from "../../layer"; |
|
import encodeSpoiler from "./encodeSpoiler"; |
|
import parseEntities from "./parseEntities"; |
|
import setBlankToAnchor from "./setBlankToAnchor"; |
|
import wrapUrl from "./wrapUrl"; |
|
|
|
/** |
|
* * Expecting correctly sorted nested entities (RichTextProcessor.sortEntities) |
|
*/ |
|
export default function wrapRichText(text: string, options: Partial<{ |
|
entities: MessageEntity[], |
|
contextSite: string, |
|
highlightUsername: string, |
|
noLinks: boolean, |
|
noLinebreaks: boolean, |
|
noCommands: boolean, |
|
wrappingDraft: boolean, |
|
//mustWrapEmoji: boolean, |
|
fromBot: boolean, |
|
noTextFormat: boolean, |
|
passEntities: Partial<{ |
|
[_ in MessageEntity['_']]: boolean |
|
}>, |
|
noEncoding: boolean, |
|
|
|
contextHashtag?: string, |
|
nasty?: { |
|
i: number, |
|
usedLength: number, |
|
text: string, |
|
lastEntity?: MessageEntity |
|
}, |
|
voodoo?: boolean |
|
}> = {}) { |
|
const fragment = document.createDocumentFragment(); |
|
if(!text) { |
|
return fragment; |
|
} |
|
|
|
const nasty = options.nasty ??= { |
|
i: 0, |
|
usedLength: 0, |
|
text |
|
}; |
|
|
|
const entities = options.entities ??= parseEntities(nasty.text); |
|
|
|
const passEntities = options.passEntities ??= {}; |
|
const contextSite = options.contextSite ??= 'Telegram'; |
|
const contextExternal = contextSite !== 'Telegram'; |
|
|
|
const textLength = nasty.text.length; |
|
const length = entities.length; |
|
let lastElement: HTMLElement | DocumentFragment; |
|
for(; nasty.i < length; ++nasty.i) { |
|
let entity = entities[nasty.i]; |
|
|
|
// * check whether text was sliced |
|
// TODO: consider about moving it to other function |
|
if(entity.offset >= textLength) { |
|
if(entity._ !== 'messageEntityCaret') { // * can set caret to the end |
|
continue; |
|
} |
|
} else if((entity.offset + entity.length) > textLength) { |
|
entity = copy(entity); |
|
entity.length = entity.offset + entity.length - textLength; |
|
} |
|
|
|
if(entity.length) { |
|
nasty.lastEntity = entity; |
|
} |
|
|
|
let nextEntity = entities[nasty.i + 1]; |
|
|
|
const startOffset = entity.offset; |
|
const endOffset = startOffset + entity.length; |
|
const endPartOffset = Math.min(endOffset, nextEntity?.offset ?? 0xFFFF); |
|
const fullEntityText = nasty.text.slice(startOffset, endOffset); |
|
const sliced = nasty.text.slice(startOffset, endPartOffset); |
|
let partText = sliced; |
|
|
|
if(nasty.usedLength < startOffset) { |
|
(lastElement || fragment).append(nasty.text.slice(nasty.usedLength, startOffset)); |
|
} |
|
|
|
if(lastElement) { |
|
lastElement = fragment; |
|
} |
|
|
|
nasty.usedLength = endPartOffset; |
|
|
|
let element: HTMLElement, |
|
property: 'textContent' | 'alt' = 'textContent', |
|
usedText = false; |
|
switch(entity._) { |
|
case 'messageEntityBold': { |
|
if(!options.noTextFormat) { |
|
if(options.wrappingDraft) { |
|
element = document.createElement('span'); |
|
element.style.fontWeight = 'bold'; |
|
} else { |
|
element = document.createElement('strong'); |
|
} |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityItalic': { |
|
if(!options.noTextFormat) { |
|
if(options.wrappingDraft) { |
|
element = document.createElement('span'); |
|
element.style.fontStyle = 'italic'; |
|
} else { |
|
element = document.createElement('em'); |
|
} |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityStrike': { |
|
if(options.wrappingDraft) { |
|
const styleName = IS_SAFARI ? 'text-decoration' : 'text-decoration-line'; |
|
element = document.createElement('span'); |
|
element.style.cssText = `${styleName}: line-through;`; |
|
} else if(!options.noTextFormat) { |
|
element = document.createElement('del'); |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityUnderline': { |
|
if(options.wrappingDraft) { |
|
const styleName = IS_SAFARI ? 'text-decoration' : 'text-decoration-line'; |
|
element = document.createElement('span'); |
|
element.style.cssText = `${styleName}: underline;`; |
|
} else if(!options.noTextFormat) { |
|
element = document.createElement('u'); |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityPre': |
|
case 'messageEntityCode': { |
|
if(options.wrappingDraft) { |
|
element = document.createElement('span'); |
|
element.style.fontFamily = 'var(--font-monospace)'; |
|
} else if(!options.noTextFormat) { |
|
element = document.createElement('code'); |
|
} |
|
|
|
break; |
|
} |
|
|
|
// case 'messageEntityPre': { |
|
// if(options.wrappingDraft) { |
|
// element = document.createElement('span'); |
|
// element.style.fontFamily = 'var(--font-monospace)'; |
|
// } else if(!options.noTextFormat) { |
|
// element = document.createElement('pre'); |
|
// const inner = document.createElement('code'); |
|
// if(entity.language) { |
|
// inner.className = 'language-' + entity.language; |
|
// inner.textContent = entityText; |
|
// usedText = true; |
|
// } |
|
// } |
|
|
|
// break; |
|
// } |
|
|
|
case 'messageEntityHighlight': { |
|
element = document.createElement('i'); |
|
element.className = 'text-highlight'; |
|
break; |
|
} |
|
|
|
case 'messageEntityBotCommand': { |
|
// if(!(options.noLinks || options.noCommands || contextExternal)/* && !entity.unsafe */) { |
|
if(!options.noLinks && passEntities[entity._]) { |
|
let command = fullEntityText.slice(1); |
|
let bot: string | boolean; |
|
let atPos: number; |
|
if((atPos = command.indexOf('@')) !== -1) { |
|
bot = command.slice(atPos + 1); |
|
command = command.slice(0, atPos); |
|
} else { |
|
bot = options.fromBot; |
|
} |
|
|
|
element = document.createElement('a'); |
|
(element as HTMLAnchorElement).href = encodeEntities('tg://bot_command?command=' + encodeURIComponent(command) + (bot ? '&bot=' + encodeURIComponent(bot) : '')); |
|
if(!contextExternal) { |
|
element.setAttribute('onclick', 'execBotCommand(this)'); |
|
} |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityEmoji': { |
|
let isSupported = IS_EMOJI_SUPPORTED; |
|
if(isSupported) { |
|
for(const version in EmojiVersions) { |
|
if(version) { |
|
const emojiData = EmojiVersions[version]; |
|
if(emojiData.hasOwnProperty(entity.unicode)) { |
|
isSupported = false; |
|
break; |
|
} |
|
} |
|
} |
|
} |
|
|
|
//if(!(options.wrappingDraft && isSupported)) { // * fix safari emoji |
|
if(!isSupported) { // no wrapping needed |
|
// if(isSupported) { // ! contenteditable="false" нужен для поля ввода, иначе там будет меняться шрифт в Safari, или же рендерить смайлик напрямую, без контейнера |
|
// insertPart(entity, '<span class="emoji">', '</span>'); |
|
// } else { |
|
element = document.createElement('img'); |
|
(element as HTMLImageElement).src = `assets/img/emoji/${entity.unicode}.png`; |
|
property = 'alt'; |
|
element.className = 'emoji'; |
|
// } |
|
//} else if(options.mustWrapEmoji) { |
|
} else if(!options.wrappingDraft) { |
|
element = document.createElement('span'); |
|
element.className = 'emoji'; |
|
}/* else if(!IS_SAFARI) { |
|
insertPart(entity, '<span class="emoji" contenteditable="false">', '</span>'); |
|
} */ |
|
/* if(!isSupported) { |
|
insertPart(entity, `<img src="assets/img/emoji/${entity.unicode}.png" alt="`, `" class="emoji">`); |
|
} */ |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityCaret': { |
|
element = document.createElement('span'); |
|
element.className = 'composer-sel'; |
|
break; |
|
} |
|
|
|
// case 'messageEntityLinebreak': { |
|
// if(options.noLinebreaks) { |
|
// insertPart(entity, ' '); |
|
// } else { |
|
// insertPart(entity, '<br/>'); |
|
// } |
|
|
|
// break; |
|
// } |
|
|
|
case 'messageEntityUrl': |
|
case 'messageEntityTextUrl': { |
|
if(!(options.noLinks && !passEntities[entity._])) { |
|
// let inner: string; |
|
let url: string = (entity as MessageEntity.messageEntityTextUrl).url || fullEntityText; |
|
let masked = false; |
|
let onclick: string; |
|
|
|
const wrapped = wrapUrl(url, true); |
|
url = wrapped.url; |
|
onclick = wrapped.onclick; |
|
|
|
if(entity._ === 'messageEntityTextUrl') { |
|
if(nextEntity?._ === 'messageEntityUrl' && |
|
nextEntity.length === entity.length && |
|
nextEntity.offset === entity.offset) { |
|
nasty.i++; |
|
} |
|
|
|
if(url !== fullEntityText) { |
|
masked = true; |
|
} |
|
} else { |
|
//inner = encodeEntities(replaceUrlEncodings(entityText)); |
|
} |
|
|
|
const currentContext = !!onclick; |
|
if(!onclick && masked && !currentContext) { |
|
onclick = 'showMaskedAlert'; |
|
} |
|
|
|
if(options.wrappingDraft) { |
|
onclick = undefined; |
|
} |
|
|
|
const href = (currentContext || typeof electronHelpers === 'undefined') |
|
? url |
|
: `javascript:electronHelpers.openExternal('${url}');`; |
|
|
|
element = document.createElement('a'); |
|
element.className = 'anchor-url'; |
|
(element as HTMLAnchorElement).href = href; |
|
|
|
if(!(currentContext || typeof electronHelpers !== 'undefined')) { |
|
setBlankToAnchor(element as HTMLAnchorElement); |
|
} |
|
|
|
if(onclick) { |
|
element.setAttribute('onclick', onclick + '(this)'); |
|
} |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityEmail': { |
|
if(!options.noLinks) { |
|
element = document.createElement('a'); |
|
(element as HTMLAnchorElement).href = encodeEntities('mailto:' + fullEntityText); |
|
setBlankToAnchor(element as HTMLAnchorElement); |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityHashtag': { |
|
const contextUrl = !options.noLinks && SITE_HASHTAGS[contextSite]; |
|
if(contextUrl) { |
|
const hashtag = fullEntityText.slice(1); |
|
element = document.createElement('a'); |
|
element.className = 'anchor-hashtag'; |
|
(element as HTMLAnchorElement).href = contextUrl.replace('{1}', encodeURIComponent(hashtag)); |
|
if(contextExternal) { |
|
setBlankToAnchor(element as HTMLAnchorElement); |
|
} else { |
|
element.setAttribute('onclick', 'searchByHashtag(this)'); |
|
} |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityMentionName': { |
|
if(!(options.noLinks && !passEntities[entity._])) { |
|
element = document.createElement('a'); |
|
(element as HTMLAnchorElement).href = buildURLHash('' + entity.user_id); |
|
element.className = 'follow'; |
|
element.dataset.follow = '' + entity.user_id; |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntityMention': { |
|
// const contextUrl = !options.noLinks && siteMentions[contextSite]; |
|
if(!options.noLinks) { |
|
const username = fullEntityText.slice(1); |
|
|
|
const {url, onclick} = wrapUrl('t.me/' + username); |
|
|
|
element = document.createElement('a'); |
|
element.className = 'mention'; |
|
(element as HTMLAnchorElement).href = url; |
|
if(onclick) { |
|
element.setAttribute('onclick', `${onclick}(this)`); |
|
} |
|
|
|
// insertPart(entity, `<a class="mention" href="${contextUrl.replace('{1}', encodeURIComponent(username))}"${contextExternal ? ' target="_blank" rel="noopener noreferrer"' : ''}>`, '</a>'); |
|
} |
|
|
|
break; |
|
} |
|
|
|
case 'messageEntitySpoiler': { |
|
if(options.noTextFormat) { |
|
const encoded = encodeSpoiler(nasty.text, entity); |
|
nasty.text = encoded.text; |
|
partText = encoded.entityText; |
|
} else if(options.wrappingDraft) { |
|
element = document.createElement('span'); |
|
element.style.fontFamily = 'spoiler'; |
|
} else { |
|
const container = document.createElement('span'); |
|
container.className = 'spoiler'; |
|
element = document.createElement('span'); |
|
element.className = 'spoiler-text'; |
|
element.textContent = partText; |
|
usedText = true; |
|
container.append(element); |
|
fragment.append(container); |
|
} |
|
|
|
break; |
|
} |
|
} |
|
|
|
if(!usedText) { |
|
if(element) { |
|
// @ts-ignore |
|
element[property] = partText; |
|
} else { |
|
(element || fragment).append(partText); |
|
} |
|
} |
|
|
|
if(element && !element.parentElement) { |
|
(lastElement || fragment).append(element); |
|
} |
|
|
|
while(nextEntity && nextEntity.offset < (endOffset - 1)) { |
|
++nasty.i; |
|
|
|
(element || fragment).append(wrapRichText(nasty.text, { |
|
...options, |
|
voodoo: true |
|
})); |
|
|
|
nextEntity = entities[nasty.i + 1]; |
|
} |
|
|
|
// if(!element?.parentElement) { |
|
// (lastElement || fragment).append(element ?? partText); |
|
// } |
|
|
|
if(entity.length > partText.length && element) { |
|
lastElement = element; |
|
} else { |
|
lastElement = fragment; |
|
} |
|
|
|
if(options.voodoo) { |
|
return fragment; |
|
} |
|
} |
|
|
|
if(nasty.lastEntity) { |
|
nasty.usedLength = nasty.lastEntity.offset + nasty.lastEntity.length; |
|
} |
|
|
|
if(nasty.usedLength < textLength) { |
|
(lastElement || fragment).append(nasty.text.slice(nasty.usedLength)); |
|
} |
|
|
|
return fragment; |
|
}
|
|
|