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.

457 lines
14 KiB

3 years ago
/*
* 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 copy from "../../helpers/object/copy";
import encodeEntities from "../../helpers/string/encodeEntities";
import { MessageEntity } from "../../layer";
import parseEntities from "./parseEntities";
import setBlankToAnchor from "./setBlankToAnchor";
import spoiler from "./spoiler";
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')
? encodeEntities(url)
: `javascript:electronHelpers.openExternal('${encodeEntities(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 = `#/im?p=${encodeURIComponent(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 before = nasty.text.slice(0, entity.offset);
const spoilerBefore = nasty.text.slice(entity.offset, entity.offset + entity.length);
const spoilerAfter = partText = spoiler(spoilerBefore)/* '▚'.repeat(entity.length) */;
const after = nasty.text.slice(entity.offset + entity.length);
nasty.text = before + spoilerAfter + after;
} 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;
}