some general project restructuring
parent
3816700703
commit
873aecbc2e
|
@ -52,7 +52,7 @@ function load_script_url(url: string) : Promise<void> {
|
|||
document.getElementById("scripts").appendChild(script_tag);
|
||||
|
||||
script_tag.src = config.baseUrl + url;
|
||||
})).then(result => {
|
||||
})).then(() => {
|
||||
/* cleanup memory */
|
||||
_script_promises[url] = Promise.resolve(); /* this promise does not holds the whole script tag and other memory */
|
||||
return _script_promises[url];
|
||||
|
|
|
@ -1,445 +1,3 @@
|
|||
import {Settings, settings} from "tc-shared/settings";
|
||||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import {spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||
import * as loader from "tc-loader";
|
||||
import * as image_preview from "./ui/frames/image_preview"
|
||||
import * as DOMPurify from "dompurify";
|
||||
|
||||
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
||||
|
||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||
import HTMLRenderer from "vendor/xbbcode/renderer/html";
|
||||
import TextRenderer from "vendor/xbbcode/renderer/text";
|
||||
|
||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||
import {TagElement, TextElement} from "vendor/xbbcode/elements";
|
||||
import * as React from "react";
|
||||
import {XBBCodeRenderer} from "vendor/xbbcode/react";
|
||||
|
||||
import * as emojiRegex from "emoji-regex";
|
||||
import * as hljs from 'highlight.js/lib/core';
|
||||
import '!style-loader!css-loader!highlight.js/styles/darcula.css';
|
||||
import {tra} from "tc-shared/i18n/localize";
|
||||
|
||||
const emojiRegexInstance = (emojiRegex as any)() as RegExp;
|
||||
|
||||
const registerLanguage = (name, language: Promise<any>) => {
|
||||
language.then(lan => hljs.registerLanguage(name, lan)).catch(error => {
|
||||
console.warn("Failed to load language %s (%o)", name, error);
|
||||
});
|
||||
};
|
||||
|
||||
registerLanguage("javascript", import("highlight.js/lib/languages/javascript"));
|
||||
registerLanguage("actionscript", import("highlight.js/lib/languages/actionscript"));
|
||||
registerLanguage("armasm", import("highlight.js/lib/languages/armasm"));
|
||||
registerLanguage("basic", import("highlight.js/lib/languages/basic"));
|
||||
registerLanguage("c-like", import("highlight.js/lib/languages/c-like"));
|
||||
registerLanguage("c", import("highlight.js/lib/languages/c"));
|
||||
registerLanguage("cmake", import("highlight.js/lib/languages/cmake"));
|
||||
registerLanguage("coffeescript", import("highlight.js/lib/languages/coffeescript"));
|
||||
registerLanguage("cpp", import("highlight.js/lib/languages/cpp"));
|
||||
registerLanguage("csharp", import("highlight.js/lib/languages/csharp"));
|
||||
registerLanguage("css", import("highlight.js/lib/languages/css"));
|
||||
registerLanguage("dart", import("highlight.js/lib/languages/dart"));
|
||||
registerLanguage("delphi", import("highlight.js/lib/languages/delphi"));
|
||||
registerLanguage("dockerfile", import("highlight.js/lib/languages/dockerfile"));
|
||||
registerLanguage("elixir", import("highlight.js/lib/languages/elixir"));
|
||||
registerLanguage("erlang", import("highlight.js/lib/languages/erlang"));
|
||||
registerLanguage("fortran", import("highlight.js/lib/languages/fortran"));
|
||||
registerLanguage("go", import("highlight.js/lib/languages/go"));
|
||||
registerLanguage("groovy", import("highlight.js/lib/languages/groovy"));
|
||||
registerLanguage("ini", import("highlight.js/lib/languages/ini"));
|
||||
registerLanguage("java", import("highlight.js/lib/languages/java"));
|
||||
registerLanguage("javascript", import("highlight.js/lib/languages/javascript"));
|
||||
registerLanguage("json", import("highlight.js/lib/languages/json"));
|
||||
registerLanguage("kotlin", import("highlight.js/lib/languages/kotlin"));
|
||||
registerLanguage("latex", import("highlight.js/lib/languages/latex"));
|
||||
registerLanguage("lua", import("highlight.js/lib/languages/lua"));
|
||||
registerLanguage("makefile", import("highlight.js/lib/languages/makefile"));
|
||||
registerLanguage("markdown", import("highlight.js/lib/languages/markdown"));
|
||||
registerLanguage("mathematica", import("highlight.js/lib/languages/mathematica"));
|
||||
registerLanguage("matlab", import("highlight.js/lib/languages/matlab"));
|
||||
registerLanguage("objectivec", import("highlight.js/lib/languages/objectivec"));
|
||||
registerLanguage("perl", import("highlight.js/lib/languages/perl"));
|
||||
registerLanguage("php", import("highlight.js/lib/languages/php"));
|
||||
registerLanguage("plaintext", import("highlight.js/lib/languages/plaintext"));
|
||||
registerLanguage("powershell", import("highlight.js/lib/languages/powershell"));
|
||||
registerLanguage("protobuf", import("highlight.js/lib/languages/protobuf"));
|
||||
registerLanguage("python", import("highlight.js/lib/languages/python"));
|
||||
registerLanguage("ruby", import("highlight.js/lib/languages/ruby"));
|
||||
registerLanguage("rust", import("highlight.js/lib/languages/rust"));
|
||||
registerLanguage("scala", import("highlight.js/lib/languages/scala"));
|
||||
registerLanguage("shell", import("highlight.js/lib/languages/shell"));
|
||||
registerLanguage("sql", import("highlight.js/lib/languages/sql"));
|
||||
registerLanguage("swift", import("highlight.js/lib/languages/swift"));
|
||||
registerLanguage("typescript", import("highlight.js/lib/languages/typescript"));
|
||||
registerLanguage("vbnet", import("highlight.js/lib/languages/vbnet"));
|
||||
registerLanguage("vbscript", import("highlight.js/lib/languages/vbscript"));
|
||||
registerLanguage("x86asm", import("highlight.js/lib/languages/x86asm"));
|
||||
registerLanguage("xml", import("highlight.js/lib/languages/xml"));
|
||||
registerLanguage("yaml", import("highlight.js/lib/languages/yaml"));
|
||||
|
||||
const rendererText = new TextRenderer();
|
||||
const rendererReact = new ReactRenderer();
|
||||
const rendererHTML = new HTMLRenderer(rendererReact);
|
||||
|
||||
export namespace bbcode {
|
||||
const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
|
||||
|
||||
export const allowedBBCodes = [
|
||||
"b", "big",
|
||||
"i", "italic",
|
||||
"u", "underlined",
|
||||
"s", "strikethrough",
|
||||
"color",
|
||||
"url",
|
||||
"code",
|
||||
"i-code", "icode",
|
||||
"sub", "sup",
|
||||
"size",
|
||||
"hr", "br",
|
||||
"left", "l", "center", "c", "right", "r",
|
||||
|
||||
"ul", "ol", "list",
|
||||
"li",
|
||||
|
||||
"table",
|
||||
"tr", "td", "th",
|
||||
|
||||
"yt", "youtube",
|
||||
"img",
|
||||
|
||||
"quote"
|
||||
];
|
||||
|
||||
export interface FormatSettings {
|
||||
is_chat_message?: boolean
|
||||
}
|
||||
|
||||
export function preprocessMessage(message: string, fsettings?: FormatSettings) {
|
||||
fsettings = fsettings || {};
|
||||
|
||||
single_url_parse:
|
||||
if(fsettings.is_chat_message) {
|
||||
/* try if its only one url */
|
||||
const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2");
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw_url);
|
||||
} catch(error) {
|
||||
break single_url_parse;
|
||||
}
|
||||
|
||||
single_url_yt:
|
||||
{
|
||||
const result = raw_url.match(yt_url_regex);
|
||||
if(!result) break single_url_yt;
|
||||
|
||||
return "[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]";
|
||||
}
|
||||
|
||||
single_url_image:
|
||||
{
|
||||
const ext_index = url.pathname.lastIndexOf(".");
|
||||
if(ext_index == -1) break single_url_image;
|
||||
|
||||
const ext_name = url.pathname.substr(ext_index + 1).toLowerCase();
|
||||
if([
|
||||
"jpeg", "jpg",
|
||||
"png", "bmp", "gif",
|
||||
"tiff", "pdf", "svg"
|
||||
].findIndex(e => e === ext_name) == -1) break single_url_image;
|
||||
|
||||
return "[img]" + message + "[/img]";
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
export function format(message: string, fsettings?: FormatSettings) : JQuery[] {
|
||||
message = preprocessMessage(message, fsettings);
|
||||
const result = parseBBCode(message, {
|
||||
tag_whitelist: allowedBBCodes
|
||||
});
|
||||
|
||||
let html = result.map(e => rendererHTML.render(e)).join("");
|
||||
/* FIXME: TODO or remove JQuery renderer
|
||||
if(settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
|
||||
html = twemoji.parse(html);
|
||||
*/
|
||||
|
||||
const container = $.spawn("div") as JQuery;
|
||||
container[0].innerHTML = html;
|
||||
|
||||
/* fixup some listeners */
|
||||
container.find("a")
|
||||
.attr('target', "_blank")
|
||||
.on('contextmenu', event => {
|
||||
if(event.isDefaultPrevented())
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
spawnUrlContextMenu(event.pageX, event.pageY, $(event.target).attr("href"));
|
||||
});
|
||||
|
||||
container.find("img").on('load', event => load_image(event.target as HTMLImageElement));
|
||||
|
||||
return [container.contents() as JQuery];
|
||||
//return result.root_tag.content.map(e => e.build_html()).map((entry, idx, array) => $.spawn("a").css("display", (idx == 0 ? "inline" : "") + "block").html(entry == "" && idx != 0 ? " " : entry));
|
||||
}
|
||||
|
||||
function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
||||
contextmenu.spawn_context_menu(pageX, pageY, {
|
||||
callback: () => {
|
||||
const win = window.open(target, '_blank');
|
||||
win.focus();
|
||||
},
|
||||
name: tr("Open URL"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-browse-addon-online"
|
||||
}, {
|
||||
callback: () => {
|
||||
//TODO
|
||||
},
|
||||
name: tr("Open URL in Browser"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
visible: __build.target === "client" && false // Currently not possible
|
||||
}, contextmenu.Entry.HR(), {
|
||||
callback: () => copy_to_clipboard(target),
|
||||
name: tr("Copy URL to clipboard"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-copy"
|
||||
});
|
||||
}
|
||||
|
||||
function load_image(entry: HTMLImageElement) {
|
||||
if(!entry.hasAttribute("x-image-url"))
|
||||
return;
|
||||
|
||||
const url = decodeURIComponent(entry.getAttribute("x-image-url") || "");
|
||||
entry.removeAttribute("x-image-url");
|
||||
|
||||
let proxiedURL;
|
||||
try {
|
||||
const parsedURL = new URL(url);
|
||||
if(parsedURL.hostname === "cdn.discordapp.com") {
|
||||
proxiedURL = url;
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
if(!proxiedURL) {
|
||||
proxiedURL = "https://images.weserv.nl/?url=" + encodeURIComponent(url);
|
||||
}
|
||||
|
||||
entry.onload = undefined;
|
||||
entry.src = proxiedURL;
|
||||
|
||||
const parent = $(entry.parentElement);
|
||||
parent.on('contextmenu', event => {
|
||||
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
|
||||
callback: () => {
|
||||
const win = window.open(url, '_blank');
|
||||
win.focus();
|
||||
},
|
||||
name: tr("Open image in browser"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-browse-addon-online"
|
||||
}, contextmenu.Entry.HR(), {
|
||||
callback: () => copy_to_clipboard(url),
|
||||
name: tr("Copy image URL to clipboard"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-copy"
|
||||
})
|
||||
});
|
||||
parent.css("cursor", "pointer").on('click', () => image_preview.preview_image(proxiedURL, url));
|
||||
}
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode code tag init",
|
||||
function: async () => {
|
||||
let reactId = 0;
|
||||
|
||||
/* override default parser */
|
||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||
tags(): string | string[] {
|
||||
return ["code", "icode", "i-code"];
|
||||
}
|
||||
|
||||
render(element: TagElement): React.ReactNode {
|
||||
const klass = element.tagNormalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
|
||||
const language = (element.options || "").replace("\"", "'").toLowerCase();
|
||||
|
||||
let lines = rendererText.renderContent(element).join("").split("\n");
|
||||
if(lines.length > 1) {
|
||||
if(lines[0].length === 0)
|
||||
lines = lines.slice(1);
|
||||
|
||||
if(lines[lines.length - 1]?.length === 0)
|
||||
lines = lines.slice(0, lines.length - 1);
|
||||
}
|
||||
|
||||
let result: HighlightJSResult;
|
||||
|
||||
const detectedLanguage = hljs.getLanguage(language);
|
||||
if(detectedLanguage)
|
||||
result = hljs.highlight(detectedLanguage.name, lines.join("\n"), true);
|
||||
else
|
||||
result = hljs.highlightAuto(lines.join("\n"));
|
||||
|
||||
return (
|
||||
<pre key={"er-" + ++reactId} className={klass}>
|
||||
<code
|
||||
className={"hljs"}
|
||||
title={tra("{} code", result.language || tr("general"))}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(result.value)
|
||||
}}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
spawn_context_menu(event.pageX, event.pageY, {
|
||||
callback: () => copy_to_clipboard(lines.join("\n")),
|
||||
name: tr("Copy code"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-copy"
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||
render(element: TagElement, renderer: ReactRenderer): React.ReactNode {
|
||||
let target;
|
||||
if (!element.options)
|
||||
target = rendererText.render(element);
|
||||
else
|
||||
target = element.options;
|
||||
|
||||
regexUrl.lastIndex = 0;
|
||||
if (!regexUrl.test(target))
|
||||
target = '#';
|
||||
|
||||
/* TODO: Implement client URLs */
|
||||
return <a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
spawnUrlContextMenu(event.pageX, event.pageY, target);
|
||||
}}>
|
||||
{renderer.renderContent(element)}
|
||||
</a>;
|
||||
}
|
||||
|
||||
tags(): string | string[] {
|
||||
return "url";
|
||||
}
|
||||
});
|
||||
|
||||
const regexImage = /^(?:https?):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||
tags(): string | string[] {
|
||||
return ["img", "image"];
|
||||
}
|
||||
|
||||
render(element: TagElement): React.ReactNode {
|
||||
let target;
|
||||
let content = rendererText.render(element);
|
||||
if (!element.options) {
|
||||
target = content;
|
||||
} else
|
||||
target = element.options;
|
||||
|
||||
regexImage.lastIndex = 0;
|
||||
if (!regexImage.test(target))
|
||||
return <React.Fragment key={"er-" + ++reactId}>{"[img]" + content + "[/img]"}</React.Fragment>;
|
||||
|
||||
return (
|
||||
<div key={"er-" + ++reactId} className={"xbbcode-tag-img"}>
|
||||
<img src={"img/loading_image.svg"} onLoad={event => load_image(event.currentTarget)} x-image-url={encodeURIComponent(target)} title={target} alt={target} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
function toCodePoint(unicodeSurrogates) {
|
||||
let r = [],
|
||||
c = 0,
|
||||
p = 0,
|
||||
i = 0;
|
||||
while (i < unicodeSurrogates.length) {
|
||||
c = unicodeSurrogates.charCodeAt(i++);
|
||||
if (p) {
|
||||
r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16));
|
||||
p = 0;
|
||||
} else if (0xD800 <= c && c <= 0xDBFF) {
|
||||
p = c;
|
||||
} else {
|
||||
r.push(c.toString(16));
|
||||
}
|
||||
}
|
||||
return r.join("-");
|
||||
}
|
||||
|
||||
const U200D = String.fromCharCode(0x200D);
|
||||
const UFE0Fg = /\uFE0F/g;
|
||||
function grabTheRightIcon(rawText) {
|
||||
// if variant is present as \uFE0F
|
||||
return toCodePoint(rawText.indexOf(U200D) < 0 ?
|
||||
rawText.replace(UFE0Fg, '') :
|
||||
rawText
|
||||
);
|
||||
}
|
||||
|
||||
rendererReact.setTextRenderer(new class extends ElementRenderer<TextElement, React.ReactNode> {
|
||||
render(element: TextElement, renderer: ReactRenderer): React.ReactNode {
|
||||
if(!settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
|
||||
return element.text();
|
||||
|
||||
let text = element.text();
|
||||
emojiRegexInstance.lastIndex = 0;
|
||||
|
||||
const result = [];
|
||||
|
||||
let lastIndex = 0;
|
||||
while(true) {
|
||||
let match = emojiRegexInstance.exec(text);
|
||||
|
||||
const rawText = text.substring(lastIndex, match?.index);
|
||||
if(rawText)
|
||||
result.push(renderer.renderAsText(rawText, false));
|
||||
|
||||
if(!match)
|
||||
break;
|
||||
|
||||
let hash = grabTheRightIcon(match[0]);
|
||||
result.push(<img key={"er-" + ++reactId} draggable={false} src={"https://twemoji.maxcdn.com/v/12.1.2/72x72/" + hash + ".png"} alt={match[0]} className={"chat-emoji"} />);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
tags(): string | string[] { return undefined; }
|
||||
});
|
||||
},
|
||||
priority: 10
|
||||
});
|
||||
}
|
||||
|
||||
export const BBCodeChatMessage = (props: { message: string }) => (
|
||||
<XBBCodeRenderer options={{ tag_whitelist: bbcode.allowedBBCodes }} renderer={rendererReact}>
|
||||
{bbcode.preprocessMessage(props.message, { is_chat_message: true })}
|
||||
</XBBCodeRenderer>
|
||||
);
|
||||
|
||||
export function sanitize_text(text: string) : string {
|
||||
return $(DOMPurify.sanitize("<a>" + text + "</a>", {
|
||||
})).text();
|
||||
}
|
||||
|
||||
export function formatDate(secs: number) : string {
|
||||
let years = Math.floor(secs / (60 * 60 * 24 * 365));
|
||||
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
|
||||
|
|
|
@ -15,12 +15,13 @@ import {
|
|||
} from "tc-shared/ui/client";
|
||||
import {ChannelEntry} from "tc-shared/ui/channel";
|
||||
import {ConnectionHandler, ConnectionState, DisconnectReason, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import {bbcode_chat, formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {server_connections} from "tc-shared/ui/frames/connection_handlers";
|
||||
import {spawnPoke} from "tc-shared/ui/modal/ModalPoke";
|
||||
import {AbstractCommandHandler, AbstractCommandHandlerBoss} from "tc-shared/connection/AbstractCommandHandler";
|
||||
import {batch_updates, BatchUpdateType, flush_batched_updates} from "tc-shared/ui/react-elements/ReactComponentBase";
|
||||
import {OutOfViewClient} from "tc-shared/ui/frames/side/PrivateConversationManager";
|
||||
import {renderBBCodeAsJQuery} from "tc-shared/text/bbcode";
|
||||
|
||||
export class ServerConnectionCommandBoss extends AbstractCommandHandlerBoss {
|
||||
constructor(connection: AbstractServerConnection) {
|
||||
|
@ -218,7 +219,7 @@ export class ConnectionCommandHandler extends AbstractCommandHandler {
|
|||
if(properties.virtualserver_hostmessage || properties.virtualserver_hostmessage_mode == 3)
|
||||
createModal({
|
||||
header: tr("Host message"),
|
||||
body: bbcode_chat(properties.virtualserver_hostmessage),
|
||||
body: renderBBCodeAsJQuery(properties.virtualserver_hostmessage, { convertSingleUrls: false }),
|
||||
footer: undefined
|
||||
}).open();
|
||||
|
||||
|
|
|
@ -1,6 +1,9 @@
|
|||
//Used by CertAccept popup
|
||||
|
||||
declare global {
|
||||
function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
|
||||
interface Array<T> {
|
||||
remove(elem?: T): boolean;
|
||||
last?(): T;
|
||||
|
|
|
@ -0,0 +1,103 @@
|
|||
import {XBBCodeRenderer} from "vendor/xbbcode/react";
|
||||
import * as React from "react";
|
||||
import {rendererHTML, rendererReact} from "tc-shared/text/bbcode/renderer";
|
||||
import {parse as parseBBCode} from "vendor/xbbcode/parser";
|
||||
import {fixupJQueryUrlTags} from "tc-shared/text/bbcode/url";
|
||||
import {fixupJQueryImageTags} from "tc-shared/text/bbcode/image";
|
||||
|
||||
export const escapeBBCode = (text: string) => text.replace(/([\[\]])/g, "\\$1");
|
||||
|
||||
export const allowedBBCodes = [
|
||||
"b", "big",
|
||||
"i", "italic",
|
||||
"u", "underlined",
|
||||
"s", "strikethrough",
|
||||
"color",
|
||||
"url",
|
||||
"code",
|
||||
"i-code", "icode",
|
||||
"sub", "sup",
|
||||
"size",
|
||||
"hr", "br",
|
||||
"left", "l", "center", "c", "right", "r",
|
||||
|
||||
"ul", "ol", "list",
|
||||
"li",
|
||||
|
||||
"table",
|
||||
"tr", "td", "th",
|
||||
|
||||
"yt", "youtube",
|
||||
"img",
|
||||
|
||||
"quote"
|
||||
];
|
||||
|
||||
export interface BBCodeRenderOptions {
|
||||
convertSingleUrls: boolean;
|
||||
}
|
||||
|
||||
const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/;
|
||||
|
||||
function preprocessMessage(message: string, settings: BBCodeRenderOptions) : string {
|
||||
/* try if its only one url */
|
||||
single_url_parse:
|
||||
if(settings.convertSingleUrls) {
|
||||
const raw_url = message.replace(/\[url(=\S+)?](\S+)\[\/url]/, "$2");
|
||||
let url: URL;
|
||||
try {
|
||||
url = new URL(raw_url);
|
||||
} catch(error) {
|
||||
break single_url_parse;
|
||||
}
|
||||
|
||||
single_url_yt:
|
||||
{
|
||||
const result = raw_url.match(yt_url_regex);
|
||||
if(!result) break single_url_yt;
|
||||
|
||||
return "[yt]https://www.youtube.com/watch?v=" + result[5] + "[/yt]";
|
||||
}
|
||||
|
||||
single_url_image:
|
||||
{
|
||||
const ext_index = url.pathname.lastIndexOf(".");
|
||||
if(ext_index == -1) break single_url_image;
|
||||
|
||||
const ext_name = url.pathname.substr(ext_index + 1).toLowerCase();
|
||||
if([
|
||||
"jpeg", "jpg",
|
||||
"png", "bmp", "gif",
|
||||
"tiff", "pdf", "svg"
|
||||
].findIndex(e => e === ext_name) == -1) break single_url_image;
|
||||
|
||||
return "[img]" + message + "[/img]";
|
||||
}
|
||||
}
|
||||
|
||||
return message;
|
||||
}
|
||||
|
||||
export const BBCodeRenderer = (props: { message: string, settings: BBCodeRenderOptions }) => (
|
||||
<XBBCodeRenderer options={{ tag_whitelist: allowedBBCodes }} renderer={rendererReact}>
|
||||
{preprocessMessage(props.message, props.settings)}
|
||||
</XBBCodeRenderer>
|
||||
);
|
||||
|
||||
|
||||
export function renderBBCodeAsJQuery(message: string, settings: BBCodeRenderOptions) : JQuery[] {
|
||||
const result = parseBBCode(preprocessMessage(message, settings), {
|
||||
tag_whitelist: allowedBBCodes
|
||||
});
|
||||
|
||||
let html = result.map(e => rendererHTML.render(e)).join("");
|
||||
|
||||
const container = $.spawn("div") as JQuery;
|
||||
container[0].innerHTML = html;
|
||||
|
||||
/* fixup some listeners */
|
||||
fixupJQueryUrlTags(container);
|
||||
fixupJQueryImageTags(container);
|
||||
|
||||
return [container.contents() as JQuery];
|
||||
}
|
|
@ -0,0 +1,80 @@
|
|||
import * as loader from "tc-loader";
|
||||
import { rendererReact } from "tc-shared/text/bbcode/renderer";
|
||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||
import {TextElement} from "vendor/xbbcode/elements";
|
||||
import * as React from "react";
|
||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
|
||||
import * as emojiRegex from "emoji-regex";
|
||||
|
||||
const emojiRegexInstance = (emojiRegex as any)() as RegExp;
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode emoji init",
|
||||
function: async () => {
|
||||
let reactId = 0;
|
||||
|
||||
function toCodePoint(unicodeSurrogates) {
|
||||
let r = [],
|
||||
c = 0,
|
||||
p = 0,
|
||||
i = 0;
|
||||
while (i < unicodeSurrogates.length) {
|
||||
c = unicodeSurrogates.charCodeAt(i++);
|
||||
if (p) {
|
||||
r.push((0x10000 + ((p - 0xD800) << 10) + (c - 0xDC00)).toString(16));
|
||||
p = 0;
|
||||
} else if (0xD800 <= c && c <= 0xDBFF) {
|
||||
p = c;
|
||||
} else {
|
||||
r.push(c.toString(16));
|
||||
}
|
||||
}
|
||||
return r.join("-");
|
||||
}
|
||||
|
||||
const U200D = String.fromCharCode(0x200D);
|
||||
const UFE0Fg = /\uFE0F/g;
|
||||
function grabTheRightIcon(rawText) {
|
||||
// if variant is present as \uFE0F
|
||||
return toCodePoint(rawText.indexOf(U200D) < 0 ?
|
||||
rawText.replace(UFE0Fg, '') :
|
||||
rawText
|
||||
);
|
||||
}
|
||||
|
||||
rendererReact.setTextRenderer(new class extends ElementRenderer<TextElement, React.ReactNode> {
|
||||
render(element: TextElement, renderer: ReactRenderer): React.ReactNode {
|
||||
if(!settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES))
|
||||
return element.text();
|
||||
|
||||
let text = element.text();
|
||||
emojiRegexInstance.lastIndex = 0;
|
||||
|
||||
const result = [];
|
||||
|
||||
let lastIndex = 0;
|
||||
while(true) {
|
||||
let match = emojiRegexInstance.exec(text);
|
||||
|
||||
const rawText = text.substring(lastIndex, match?.index);
|
||||
if(rawText)
|
||||
result.push(renderer.renderAsText(rawText, false));
|
||||
|
||||
if(!match)
|
||||
break;
|
||||
|
||||
let hash = grabTheRightIcon(match[0]);
|
||||
result.push(<img key={"er-" + ++reactId} draggable={false} src={"https://twemoji.maxcdn.com/v/12.1.2/72x72/" + hash + ".png"} alt={match[0]} className={"chat-emoji"} />);
|
||||
lastIndex = match.index + match[0].length;
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
tags(): string | string[] { return undefined; }
|
||||
});
|
||||
},
|
||||
priority: 10
|
||||
});
|
|
@ -0,0 +1,126 @@
|
|||
import * as hljs from "highlight.js/lib/core";
|
||||
import * as loader from "tc-loader";
|
||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||
import {TagElement} from "vendor/xbbcode/elements";
|
||||
import * as React from "react";
|
||||
import {tra} from "tc-shared/i18n/localize";
|
||||
import * as DOMPurify from "dompurify";
|
||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||
import {MenuEntryType, spawn_context_menu} from "tc-shared/ui/elements/ContextMenu";
|
||||
|
||||
import '!style-loader!css-loader!highlight.js/styles/darcula.css';
|
||||
|
||||
const registerLanguage = (name, language: Promise<any>) => {
|
||||
language.then(lan => hljs.registerLanguage(name, lan)).catch(error => {
|
||||
console.warn("Failed to load language %s (%o)", name, error);
|
||||
});
|
||||
};
|
||||
|
||||
registerLanguage("javascript", import("highlight.js/lib/languages/javascript"));
|
||||
registerLanguage("actionscript", import("highlight.js/lib/languages/actionscript"));
|
||||
registerLanguage("armasm", import("highlight.js/lib/languages/armasm"));
|
||||
registerLanguage("basic", import("highlight.js/lib/languages/basic"));
|
||||
registerLanguage("c-like", import("highlight.js/lib/languages/c-like"));
|
||||
registerLanguage("c", import("highlight.js/lib/languages/c"));
|
||||
registerLanguage("cmake", import("highlight.js/lib/languages/cmake"));
|
||||
registerLanguage("coffeescript", import("highlight.js/lib/languages/coffeescript"));
|
||||
registerLanguage("cpp", import("highlight.js/lib/languages/cpp"));
|
||||
registerLanguage("csharp", import("highlight.js/lib/languages/csharp"));
|
||||
registerLanguage("css", import("highlight.js/lib/languages/css"));
|
||||
registerLanguage("dart", import("highlight.js/lib/languages/dart"));
|
||||
registerLanguage("delphi", import("highlight.js/lib/languages/delphi"));
|
||||
registerLanguage("dockerfile", import("highlight.js/lib/languages/dockerfile"));
|
||||
registerLanguage("elixir", import("highlight.js/lib/languages/elixir"));
|
||||
registerLanguage("erlang", import("highlight.js/lib/languages/erlang"));
|
||||
registerLanguage("fortran", import("highlight.js/lib/languages/fortran"));
|
||||
registerLanguage("go", import("highlight.js/lib/languages/go"));
|
||||
registerLanguage("groovy", import("highlight.js/lib/languages/groovy"));
|
||||
registerLanguage("ini", import("highlight.js/lib/languages/ini"));
|
||||
registerLanguage("java", import("highlight.js/lib/languages/java"));
|
||||
registerLanguage("javascript", import("highlight.js/lib/languages/javascript"));
|
||||
registerLanguage("json", import("highlight.js/lib/languages/json"));
|
||||
registerLanguage("kotlin", import("highlight.js/lib/languages/kotlin"));
|
||||
registerLanguage("latex", import("highlight.js/lib/languages/latex"));
|
||||
registerLanguage("lua", import("highlight.js/lib/languages/lua"));
|
||||
registerLanguage("makefile", import("highlight.js/lib/languages/makefile"));
|
||||
registerLanguage("markdown", import("highlight.js/lib/languages/markdown"));
|
||||
registerLanguage("mathematica", import("highlight.js/lib/languages/mathematica"));
|
||||
registerLanguage("matlab", import("highlight.js/lib/languages/matlab"));
|
||||
registerLanguage("objectivec", import("highlight.js/lib/languages/objectivec"));
|
||||
registerLanguage("perl", import("highlight.js/lib/languages/perl"));
|
||||
registerLanguage("php", import("highlight.js/lib/languages/php"));
|
||||
registerLanguage("plaintext", import("highlight.js/lib/languages/plaintext"));
|
||||
registerLanguage("powershell", import("highlight.js/lib/languages/powershell"));
|
||||
registerLanguage("protobuf", import("highlight.js/lib/languages/protobuf"));
|
||||
registerLanguage("python", import("highlight.js/lib/languages/python"));
|
||||
registerLanguage("ruby", import("highlight.js/lib/languages/ruby"));
|
||||
registerLanguage("rust", import("highlight.js/lib/languages/rust"));
|
||||
registerLanguage("scala", import("highlight.js/lib/languages/scala"));
|
||||
registerLanguage("shell", import("highlight.js/lib/languages/shell"));
|
||||
registerLanguage("sql", import("highlight.js/lib/languages/sql"));
|
||||
registerLanguage("swift", import("highlight.js/lib/languages/swift"));
|
||||
registerLanguage("typescript", import("highlight.js/lib/languages/typescript"));
|
||||
registerLanguage("vbnet", import("highlight.js/lib/languages/vbnet"));
|
||||
registerLanguage("vbscript", import("highlight.js/lib/languages/vbscript"));
|
||||
registerLanguage("x86asm", import("highlight.js/lib/languages/x86asm"));
|
||||
registerLanguage("xml", import("highlight.js/lib/languages/xml"));
|
||||
registerLanguage("yaml", import("highlight.js/lib/languages/yaml"));
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode highlight init",
|
||||
function: async () => {
|
||||
let reactId = 0;
|
||||
|
||||
/* override default parser */
|
||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||
tags(): string | string[] {
|
||||
return ["code", "icode", "i-code"];
|
||||
}
|
||||
|
||||
render(element: TagElement): React.ReactNode {
|
||||
const klass = element.tagNormalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code";
|
||||
const language = (element.options || "").replace("\"", "'").toLowerCase();
|
||||
|
||||
let lines = rendererText.renderContent(element).join("").split("\n");
|
||||
if(lines.length > 1) {
|
||||
if(lines[0].length === 0)
|
||||
lines = lines.slice(1);
|
||||
|
||||
if(lines[lines.length - 1]?.length === 0)
|
||||
lines = lines.slice(0, lines.length - 1);
|
||||
}
|
||||
|
||||
let result: HighlightJSResult;
|
||||
|
||||
const detectedLanguage = hljs.getLanguage(language);
|
||||
if(detectedLanguage)
|
||||
result = hljs.highlight(detectedLanguage.name, lines.join("\n"), true);
|
||||
else
|
||||
result = hljs.highlightAuto(lines.join("\n"));
|
||||
|
||||
return (
|
||||
<pre key={"hrc-" + ++reactId} className={klass}>
|
||||
<code
|
||||
className={"hljs"}
|
||||
title={tra("{} code", result.language || tr("general"))}
|
||||
dangerouslySetInnerHTML={{
|
||||
__html: DOMPurify.sanitize(result.value)
|
||||
}}
|
||||
onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
spawn_context_menu(event.pageX, event.pageY, {
|
||||
callback: () => copy_to_clipboard(lines.join("\n")),
|
||||
name: tr("Copy code"),
|
||||
type: MenuEntryType.ENTRY,
|
||||
icon_class: "client-copy"
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
priority: 10
|
||||
});
|
|
@ -0,0 +1,89 @@
|
|||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||
import {TagElement} from "vendor/xbbcode/elements";
|
||||
import * as React from "react";
|
||||
import * as loader from "tc-loader";
|
||||
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||
import * as image_preview from "tc-shared/ui/frames/image_preview";
|
||||
|
||||
const regexImage = /^(?:https?):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
||||
|
||||
function loadImageForElement(element: HTMLImageElement) {
|
||||
if(!element.hasAttribute("x-image-url"))
|
||||
return;
|
||||
|
||||
const url = decodeURIComponent(element.getAttribute("x-image-url") || "");
|
||||
element.removeAttribute("x-image-url");
|
||||
|
||||
let proxiedURL;
|
||||
try {
|
||||
const parsedURL = new URL(url);
|
||||
if(parsedURL.hostname === "cdn.discordapp.com") {
|
||||
proxiedURL = url;
|
||||
}
|
||||
} catch (e) { }
|
||||
|
||||
if(!proxiedURL) {
|
||||
proxiedURL = "https://images.weserv.nl/?url=" + encodeURIComponent(url);
|
||||
}
|
||||
|
||||
element.onload = undefined;
|
||||
element.src = proxiedURL;
|
||||
|
||||
const parent = $(element.parentElement);
|
||||
parent.on('contextmenu', event => {
|
||||
contextmenu.spawn_context_menu(event.pageX, event.pageY, {
|
||||
callback: () => {
|
||||
const win = window.open(url, '_blank');
|
||||
win.focus();
|
||||
},
|
||||
name: tr("Open image in browser"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-browse-addon-online"
|
||||
}, contextmenu.Entry.HR(), {
|
||||
callback: () => copy_to_clipboard(url),
|
||||
name: tr("Copy image URL to clipboard"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-copy"
|
||||
})
|
||||
});
|
||||
parent.css("cursor", "pointer").on('click', () => image_preview.preview_image(proxiedURL, url));
|
||||
}
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode emoji init",
|
||||
function: async () => {
|
||||
let reactId = 0;
|
||||
|
||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||
tags(): string | string[] {
|
||||
return ["img", "image"];
|
||||
}
|
||||
|
||||
render(element: TagElement): React.ReactNode {
|
||||
let target;
|
||||
let content = rendererText.render(element);
|
||||
if (!element.options) {
|
||||
target = content;
|
||||
} else
|
||||
target = element.options;
|
||||
|
||||
regexImage.lastIndex = 0;
|
||||
if (!regexImage.test(target))
|
||||
return <React.Fragment key={"er-" + ++reactId}>{"[img]" + content + "[/img]"}</React.Fragment>;
|
||||
|
||||
return (
|
||||
<div key={"irc-" + ++reactId} className={"xbbcode-tag-img"}>
|
||||
<img src={"img/loading_image.svg"} onLoad={event => loadImageForElement(event.currentTarget)} x-image-url={encodeURIComponent(target)} title={target} alt={target} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
},
|
||||
priority: 10
|
||||
});
|
||||
|
||||
export function fixupJQueryImageTags(container: JQuery) {
|
||||
container.find("img").on('load', event => loadImageForElement(event.target as HTMLImageElement));
|
||||
}
|
|
@ -0,0 +1,10 @@
|
|||
import TextRenderer from "vendor/xbbcode/renderer/text";
|
||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||
import HTMLRenderer from "vendor/xbbcode/renderer/html";
|
||||
|
||||
import "./emoji";
|
||||
import "./highlight";
|
||||
|
||||
export const rendererText = new TextRenderer();
|
||||
export const rendererReact = new ReactRenderer();
|
||||
export const rendererHTML = new HTMLRenderer(rendererReact);
|
|
@ -0,0 +1,78 @@
|
|||
import * as contextmenu from "tc-shared/ui/elements/ContextMenu";
|
||||
import {copy_to_clipboard} from "tc-shared/utils/helpers";
|
||||
import * as loader from "tc-loader";
|
||||
import {ElementRenderer} from "vendor/xbbcode/renderer/base";
|
||||
import {TagElement} from "vendor/xbbcode/elements";
|
||||
import * as React from "react";
|
||||
import ReactRenderer from "vendor/xbbcode/renderer/react";
|
||||
import {rendererReact, rendererText} from "tc-shared/text/bbcode/renderer";
|
||||
|
||||
function spawnUrlContextMenu(pageX: number, pageY: number, target: string) {
|
||||
contextmenu.spawn_context_menu(pageX, pageY, {
|
||||
callback: () => {
|
||||
const win = window.open(target, '_blank');
|
||||
win.focus();
|
||||
},
|
||||
name: tr("Open URL"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-browse-addon-online"
|
||||
}, {
|
||||
callback: () => {
|
||||
//TODO
|
||||
},
|
||||
name: tr("Open URL in Browser"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
visible: __build.target === "client" && false // Currently not possible
|
||||
}, contextmenu.Entry.HR(), {
|
||||
callback: () => copy_to_clipboard(target),
|
||||
name: tr("Copy URL to clipboard"),
|
||||
type: contextmenu.MenuEntryType.ENTRY,
|
||||
icon_class: "client-copy"
|
||||
});
|
||||
}
|
||||
|
||||
loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, {
|
||||
name: "XBBCode code tag init",
|
||||
function: async () => {
|
||||
let reactId = 0;
|
||||
|
||||
const regexUrl = /^(?:[a-zA-Z]{1,16}):(?:\/{1,3}|\\)[-a-zA-Z0-9:;,@#%&()~_?+=\/\\.]*$/g;
|
||||
rendererReact.registerCustomRenderer(new class extends ElementRenderer<TagElement, React.ReactNode> {
|
||||
render(element: TagElement, renderer: ReactRenderer): React.ReactNode {
|
||||
let target;
|
||||
if (!element.options)
|
||||
target = rendererText.render(element);
|
||||
else
|
||||
target = element.options;
|
||||
|
||||
regexUrl.lastIndex = 0;
|
||||
if (!regexUrl.test(target))
|
||||
target = '#';
|
||||
|
||||
/* TODO: Implement client URLs */
|
||||
return <a key={"er-" + ++reactId} className={"xbbcode xbbcode-tag-url"} href={target} target={"_blank"} onContextMenu={event => {
|
||||
event.preventDefault();
|
||||
spawnUrlContextMenu(event.pageX, event.pageY, target);
|
||||
}}>
|
||||
{renderer.renderContent(element)}
|
||||
</a>;
|
||||
}
|
||||
|
||||
tags(): string | string[] {
|
||||
return "url";
|
||||
}
|
||||
});
|
||||
},
|
||||
priority: 10
|
||||
});
|
||||
|
||||
|
||||
export function fixupJQueryUrlTags(container: JQuery) {
|
||||
container.find("a").on('contextmenu', event => {
|
||||
if(event.isDefaultPrevented())
|
||||
return;
|
||||
|
||||
event.preventDefault();
|
||||
spawnUrlContextMenu(event.pageX, event.pageY, $(event.target).attr("href"));
|
||||
});
|
||||
}
|
|
@ -0,0 +1,69 @@
|
|||
//https://regex101.com/r/YQbfcX/2
|
||||
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
|
||||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
import {renderMarkdownAsBBCode} from "tc-shared/text/markdown";
|
||||
import {escapeBBCode} from "tc-shared/text/bbcode";
|
||||
|
||||
const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
|
||||
function process_urls(message: string) : string {
|
||||
const words = message.split(/[ \n]/);
|
||||
for(let index = 0; index < words.length; index++) {
|
||||
const flag_escaped = words[index].startsWith('!');
|
||||
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
|
||||
|
||||
_try:
|
||||
try {
|
||||
const url = new URL(unescaped);
|
||||
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
|
||||
if(url.protocol !== 'http:' && url.protocol !== 'https:')
|
||||
break _try;
|
||||
if(flag_escaped) {
|
||||
message = undefined;
|
||||
words[index] = unescaped;
|
||||
} else {
|
||||
message = undefined;
|
||||
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
|
||||
}
|
||||
} catch(e) { /* word isn't an url */ }
|
||||
|
||||
if(unescaped.match(URL_REGEX)) {
|
||||
if(flag_escaped) {
|
||||
message = undefined;
|
||||
words[index] = unescaped;
|
||||
} else {
|
||||
message = undefined;
|
||||
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message || words.join(" ");
|
||||
}
|
||||
|
||||
export function preprocess_chat_message(message: string) : string {
|
||||
const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS);
|
||||
const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN);
|
||||
const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE);
|
||||
|
||||
if(parse_markdown) {
|
||||
return renderMarkdownAsBBCode(message, text => {
|
||||
if(escape_bb)
|
||||
text = escapeBBCode(text);
|
||||
|
||||
if(process_url)
|
||||
text = process_urls(text);
|
||||
|
||||
return text;
|
||||
});
|
||||
}
|
||||
|
||||
if(escape_bb)
|
||||
message = escapeBBCode(message);
|
||||
|
||||
if(process_url)
|
||||
message = process_urls(message);
|
||||
|
||||
return message;
|
||||
}
|
|
@ -0,0 +1,149 @@
|
|||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {
|
||||
CodeToken, Env, FenceToken, HeadingOpenToken,
|
||||
ImageToken,
|
||||
LinkOpenToken, Options,
|
||||
ParagraphOpenToken,
|
||||
SubToken,
|
||||
SupToken,
|
||||
TextToken,
|
||||
Token
|
||||
} from "remarkable/lib";
|
||||
import {escapeBBCode} from "tc-shared/text/BBCodeHelper";
|
||||
const { Remarkable } = require("remarkable");
|
||||
|
||||
export class MD2BBCodeRenderer {
|
||||
private static renderers: {[key: string]:(renderer: MD2BBCodeRenderer, token: Token) => string} = {
|
||||
"text": (renderer: MD2BBCodeRenderer, token: TextToken) => renderer.options().textProcessor(token.content),
|
||||
"softbreak": () => "\n",
|
||||
"hardbreak": () => "\n",
|
||||
|
||||
"paragraph_open": (renderer: MD2BBCodeRenderer, token: ParagraphOpenToken) => {
|
||||
const last_line = !renderer.last_paragraph || !renderer.last_paragraph.lines ? 0 : renderer.last_paragraph.lines[1];
|
||||
const lines = token.lines[0] - last_line;
|
||||
return [...new Array(lines)].map(() => "[br]").join("");
|
||||
},
|
||||
"paragraph_close": () => "",
|
||||
|
||||
"strong_open": () => "[b]",
|
||||
"strong_close": () => "[/b]",
|
||||
|
||||
"em_open": () => "[i]",
|
||||
"em_close": () => "[/i]",
|
||||
|
||||
"del_open": () => "[s]",
|
||||
"del_close": () => "[/s]",
|
||||
|
||||
"sup": (renderer: MD2BBCodeRenderer, token: SupToken) => "[sup]" + renderer.options().textProcessor(token.content) + "[/sup]",
|
||||
"sub": (renderer: MD2BBCodeRenderer, token: SubToken) => "[sub]" + renderer.options().textProcessor(token.content) + "[/sub]",
|
||||
|
||||
"bullet_list_open": () => "[ul]",
|
||||
"bullet_list_close": () => "[/ul]",
|
||||
|
||||
"ordered_list_open": () => "[ol]",
|
||||
"ordered_list_close": () => "[/ol]",
|
||||
|
||||
"list_item_open": () => "[li]",
|
||||
"list_item_close": () => "[/li]",
|
||||
|
||||
"table_open": () => "[table]",
|
||||
"table_close": () => "[/table]",
|
||||
|
||||
"thead_open": () => "",
|
||||
"thead_close": () => "",
|
||||
|
||||
"tbody_open": () => "",
|
||||
"tbody_close": () => "",
|
||||
|
||||
"tr_open": () => "[tr]",
|
||||
"tr_close": () => "[/tr]",
|
||||
|
||||
"th_open": (renderer: MD2BBCodeRenderer, token: any) => "[th" + (token.align ? ("=" + token.align) : "") + "]",
|
||||
"th_close": () => "[/th]",
|
||||
|
||||
"td_open": () => "[td]",
|
||||
"td_close": () => "[/td]",
|
||||
|
||||
"link_open": (renderer: MD2BBCodeRenderer, token: LinkOpenToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]",
|
||||
"link_close": () => "[/url]",
|
||||
|
||||
"image": (renderer: MD2BBCodeRenderer, token: ImageToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]",
|
||||
|
||||
//footnote_ref
|
||||
|
||||
//"content": "==Marked text==",
|
||||
//mark_open
|
||||
//mark_close
|
||||
|
||||
//++Inserted text++
|
||||
"ins_open": () => "[u]",
|
||||
"ins_close": () => "[/u]",
|
||||
|
||||
"code": (renderer: MD2BBCodeRenderer, token: CodeToken) => "[i-code]" + escapeBBCode(token.content) + "[/i-code]",
|
||||
"fence": (renderer: MD2BBCodeRenderer, token: FenceToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + escapeBBCode(token.content) + "[/code]",
|
||||
|
||||
"heading_open": (renderer: MD2BBCodeRenderer, token: HeadingOpenToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]",
|
||||
"heading_close": () => "[/size][hr]",
|
||||
|
||||
"hr": () => "[hr]",
|
||||
|
||||
//> Experience real-time editing with Remarkable!
|
||||
"blockquote_open": () => "[quote]",
|
||||
"blockquote_close": () => "[/quote]"
|
||||
};
|
||||
|
||||
private _options;
|
||||
last_paragraph: Token;
|
||||
|
||||
render(tokens: Token[], options: Options, env: Env): string {
|
||||
this.last_paragraph = undefined;
|
||||
this._options = options;
|
||||
let result = '';
|
||||
|
||||
for(let index = 0; index < tokens.length; index++) {
|
||||
if (tokens[index].type === 'inline') {
|
||||
/* we're just ignoring the inline fact */
|
||||
result += this.render((tokens[index] as any).children, options, env);
|
||||
} else {
|
||||
result += this.renderToken(tokens[index]);
|
||||
}
|
||||
}
|
||||
|
||||
this._options = undefined;
|
||||
return result;
|
||||
}
|
||||
|
||||
private renderToken(token: Token) {
|
||||
log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token);
|
||||
const renderer = MD2BBCodeRenderer.renderers[token.type];
|
||||
if(typeof(renderer) === "undefined") {
|
||||
log.warn(LogCategory.CHAT, tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token);
|
||||
return 'content' in token ? this.options().textProcessor(token.content) : "";
|
||||
}
|
||||
|
||||
const result = renderer(this, token);
|
||||
if(token.type === "paragraph_open")
|
||||
this.last_paragraph = token;
|
||||
return result;
|
||||
}
|
||||
|
||||
options() : any {
|
||||
return this._options;
|
||||
}
|
||||
}
|
||||
|
||||
const remarkableRenderer = new Remarkable("full", {
|
||||
typographer: true
|
||||
});
|
||||
remarkableRenderer.renderer = new MD2BBCodeRenderer() as any;
|
||||
remarkableRenderer.inline.ruler.disable([ 'newline', 'autolink' ]);
|
||||
|
||||
export function renderMarkdownAsBBCode(message: string, textProcessor: (text: string) => string) : string {
|
||||
remarkableRenderer.set({ textProcessor: textProcessor } as any);
|
||||
|
||||
let result = remarkableRenderer.render(message);
|
||||
if(result.endsWith("\n"))
|
||||
result = result.substr(0, result.length - 1);
|
||||
return result;
|
||||
}
|
|
@ -1,10 +1,7 @@
|
|||
import {LogCategory} from "tc-shared/log";
|
||||
import {settings, Settings} from "tc-shared/settings";
|
||||
import * as log from "tc-shared/log";
|
||||
import {bbcode} from "tc-shared/MessageFormatter";
|
||||
import * as loader from "tc-loader";
|
||||
import { XBBCodeRenderer } from "vendor/xbbcode/react";
|
||||
import * as React from "react";
|
||||
|
||||
export enum ChatType {
|
||||
GENERAL,
|
||||
|
@ -137,14 +134,6 @@ export function formatMessageString(pattern: string, ...args: string[]) : string
|
|||
return result.join("");
|
||||
}
|
||||
|
||||
//TODO: Remove this (only legacy)
|
||||
export function bbcode_chat(message: string) : JQuery[] {
|
||||
return bbcode.format(message, {
|
||||
is_chat_message: true
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
export namespace network {
|
||||
export const KB = 1024;
|
||||
export const MB = 1024 * KB;
|
||||
|
|
|
@ -10,9 +10,6 @@ import {MusicInfo} from "tc-shared/ui/frames/side/music_info";
|
|||
import {ConversationManager} from "tc-shared/ui/frames/side/ConversationManager";
|
||||
import {PrivateConversationManager} from "tc-shared/ui/frames/side/PrivateConversationManager";
|
||||
|
||||
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
|
||||
export enum InfoFrameMode {
|
||||
NONE = "none",
|
||||
CHANNEL_CHAT = "channel_chat",
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
import {tra} from "tc-shared/i18n/localize";
|
||||
import {tra, traj} from "tc-shared/i18n/localize";
|
||||
import {PermissionInfo} from "tc-shared/permission/PermissionManager";
|
||||
import {ConnectionHandler, ViewReasonId} from "tc-shared/ConnectionHandler";
|
||||
import * as htmltags from "tc-shared/ui/htmltags";
|
||||
import {bbcode_chat, format_time, formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {format_time, formatMessage} from "tc-shared/ui/frames/chat";
|
||||
import {formatDate} from "tc-shared/MessageFormatter";
|
||||
import {CommandResult} from "tc-shared/connection/ServerConnectionDeclaration";
|
||||
import {BBCodeRenderOptions, renderBBCodeAsJQuery} from "tc-shared/text/bbcode";
|
||||
|
||||
export enum Type {
|
||||
CONNECTION_BEGIN = "connection_begin",
|
||||
|
@ -259,7 +260,7 @@ export interface TypeInfo {
|
|||
}
|
||||
|
||||
export type MessageBuilderOptions = {};
|
||||
export type MessageBuilder<T extends keyof TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | undefined;
|
||||
export type MessageBuilder<T extends keyof TypeInfo> = (data: TypeInfo[T], options: MessageBuilderOptions) => JQuery[] | string | undefined;
|
||||
|
||||
export const MessageBuilders: {[key: string]: MessageBuilder<any>} = {
|
||||
"error_custom": (data: event.ErrorCustom) => {
|
||||
|
@ -513,12 +514,16 @@ MessageBuilders["client_view_leave"] = (data: event.ClientLeave) => {
|
|||
return [$.spawn("div").addClass("log-error").text("Invalid view leave reason id (" + data.reason + ")")];
|
||||
};
|
||||
|
||||
const bbcodeRenderOptions: BBCodeRenderOptions = {
|
||||
convertSingleUrls: false
|
||||
};
|
||||
|
||||
MessageBuilders["server_welcome_message"] = (data: event.WelcomeMessage) => {
|
||||
return bbcode_chat("[color=green]" + data.message + "[/color]");
|
||||
return renderBBCodeAsJQuery("[color=green]" + data.message + "[/color]", bbcodeRenderOptions);
|
||||
};
|
||||
|
||||
MessageBuilders["server_host_message"] = (data: event.WelcomeMessage) => {
|
||||
return bbcode_chat("[color=green]" + data.message + "[/color]");
|
||||
return renderBBCodeAsJQuery("[color=green]" + data.message + "[/color]", bbcodeRenderOptions);
|
||||
};
|
||||
|
||||
MessageBuilders["client_nickname_changed"] = (data: event.ClientNicknameChanged) => {
|
||||
|
@ -557,14 +562,14 @@ MessageBuilders["server_banned"] = (data: event.ServerBanned) => {
|
|||
const time = data.time == 0 ? tr("ever") : format_time(data.time * 1000, tr("one second"));
|
||||
if(data.invoker.client_id > 0) {
|
||||
if(data.message)
|
||||
result = tra("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message);
|
||||
result = traj("You've been banned from the server by {0} for {1}. Reason: {2}", client_tag(data.invoker), time, data.message);
|
||||
else
|
||||
result = tra("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time);
|
||||
result = traj("You've been banned from the server by {0} for {1}.", client_tag(data.invoker), time);
|
||||
} else {
|
||||
if(data.message)
|
||||
result = tra("You've been banned from the server for {0}. Reason: {1}", time, data.message);
|
||||
result = traj("You've been banned from the server for {0}. Reason: {1}", time, data.message);
|
||||
else
|
||||
result = tra("You've been banned from the server for {0}.", time);
|
||||
result = traj("You've been banned from the server for {0}.", time);
|
||||
}
|
||||
|
||||
return result.map(e => e.addClass("log-error"));
|
||||
|
|
|
@ -66,8 +66,9 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
return;
|
||||
|
||||
if(this.activeClient instanceof ClientEntry) {
|
||||
this.activeClient.setUnread(false); /* clear the unread flag */
|
||||
|
||||
if(client instanceof ClientEntry) {
|
||||
this.activeClient.setUnread(false); /* "transfer" the unread flag */
|
||||
this.registerChatEvent({
|
||||
type: "partner-instance-changed",
|
||||
oldClient: this.activeClient.clientNickName(),
|
||||
|
@ -87,7 +88,7 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
}
|
||||
|
||||
hasUnreadMessages() : boolean {
|
||||
return false;
|
||||
return this.unreadTimestamp !== undefined;
|
||||
}
|
||||
|
||||
handleIncomingMessage(client: ClientEntry | OutOfViewClient, isOwnMessage: boolean, message: ChatMessage) {
|
||||
|
@ -245,6 +246,14 @@ export class PrivateConversation extends AbstractChat<PrivateConversationUIEvent
|
|||
this.sendMessageSendingEnabled(this.lastClientInfo.clientId !== 0);
|
||||
}
|
||||
|
||||
setUnreadTimestamp(timestamp: number | undefined) {
|
||||
super.setUnreadTimestamp(timestamp);
|
||||
|
||||
/* TODO: Move this somehow to the client itself? */
|
||||
if(this.activeClient instanceof ClientEntry)
|
||||
this.activeClient.setUnread(timestamp === undefined);
|
||||
}
|
||||
|
||||
protected canClientAccessChat(): boolean {
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,215 +1,10 @@
|
|||
import * as log from "tc-shared/log";
|
||||
import {LogCategory} from "tc-shared/log";
|
||||
import {Settings, settings} from "tc-shared/settings";
|
||||
const { Remarkable } = require("remarkable");
|
||||
console.error(Remarkable);
|
||||
|
||||
const escapeBBCode = (text: string) => text.replace(/([\[\]])/g, "\\$1");
|
||||
|
||||
export namespace helpers {
|
||||
//https://regex101.com/r/YQbfcX/2
|
||||
//static readonly URL_REGEX = /^(?<hostname>([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/(?<path>(?:[^\s?]+)?)(?:\?(?<query>\S+))?)?$/gm;
|
||||
const URL_REGEX = /^(([a-zA-Z0-9-]+\.)+[a-zA-Z0-9-]{2,63})(?:\/((?:[^\s?]+)?)(?:\?(\S+))?)?$/gm;
|
||||
function process_urls(message: string) : string {
|
||||
const words = message.split(/[ \n]/);
|
||||
for(let index = 0; index < words.length; index++) {
|
||||
const flag_escaped = words[index].startsWith('!');
|
||||
const unescaped = flag_escaped ? words[index].substr(1) : words[index];
|
||||
|
||||
_try:
|
||||
try {
|
||||
const url = new URL(unescaped);
|
||||
log.debug(LogCategory.GENERAL, tr("Chat message contains URL: %o"), url);
|
||||
if(url.protocol !== 'http:' && url.protocol !== 'https:')
|
||||
break _try;
|
||||
if(flag_escaped) {
|
||||
message = undefined;
|
||||
words[index] = unescaped;
|
||||
} else {
|
||||
message = undefined;
|
||||
words[index] = "[url=" + url.toString() + "]" + url.toString() + "[/url]";
|
||||
}
|
||||
} catch(e) { /* word isn't an url */ }
|
||||
|
||||
if(unescaped.match(URL_REGEX)) {
|
||||
if(flag_escaped) {
|
||||
message = undefined;
|
||||
words[index] = unescaped;
|
||||
} else {
|
||||
message = undefined;
|
||||
words[index] = "[url=" + unescaped + "]" + unescaped + "[/url]";
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return message || words.join(" ");
|
||||
}
|
||||
|
||||
export class MD2BBCodeRenderer {
|
||||
private static renderers: {[key: string]:(renderer: MD2BBCodeRenderer, token: Remarkable.Token) => string} = {
|
||||
"text": (renderer: MD2BBCodeRenderer, token: Remarkable.TextToken) => renderer.options().process_url ? process_urls(renderer.maybe_escape_bb(token.content)) : renderer.maybe_escape_bb(token.content),
|
||||
"softbreak": () => "\n",
|
||||
"hardbreak": () => "\n",
|
||||
|
||||
"paragraph_open": (renderer: MD2BBCodeRenderer, token: Remarkable.ParagraphOpenToken) => {
|
||||
const last_line = !renderer.last_paragraph || !renderer.last_paragraph.lines ? 0 : renderer.last_paragraph.lines[1];
|
||||
const lines = token.lines[0] - last_line;
|
||||
return [...new Array(lines)].map(() => "[br]").join("");
|
||||
},
|
||||
"paragraph_close": () => "",
|
||||
|
||||
"strong_open": () => "[b]",
|
||||
"strong_close": () => "[/b]",
|
||||
|
||||
"em_open": () => "[i]",
|
||||
"em_close": () => "[/i]",
|
||||
|
||||
"del_open": () => "[s]",
|
||||
"del_close": () => "[/s]",
|
||||
|
||||
"sup": (renderer: MD2BBCodeRenderer, token: Remarkable.SupToken) => "[sup]" + renderer.maybe_escape_bb(token.content) + "[/sup]",
|
||||
"sub": (renderer: MD2BBCodeRenderer, token: Remarkable.SubToken) => "[sub]" + renderer.maybe_escape_bb(token.content) + "[/sub]",
|
||||
|
||||
"bullet_list_open": () => "[ul]",
|
||||
"bullet_list_close": () => "[/ul]",
|
||||
|
||||
"ordered_list_open": () => "[ol]",
|
||||
"ordered_list_close": () => "[/ol]",
|
||||
|
||||
"list_item_open": () => "[li]",
|
||||
"list_item_close": () => "[/li]",
|
||||
|
||||
"table_open": () => "[table]",
|
||||
"table_close": () => "[/table]",
|
||||
|
||||
"thead_open": () => "",
|
||||
"thead_close": () => "",
|
||||
|
||||
"tbody_open": () => "",
|
||||
"tbody_close": () => "",
|
||||
|
||||
"tr_open": () => "[tr]",
|
||||
"tr_close": () => "[/tr]",
|
||||
|
||||
"th_open": (renderer: MD2BBCodeRenderer, token: any) => "[th" + (token.align ? ("=" + token.align) : "") + "]",
|
||||
"th_close": () => "[/th]",
|
||||
|
||||
"td_open": () => "[td]",
|
||||
"td_close": () => "[/td]",
|
||||
|
||||
"link_open": (renderer: MD2BBCodeRenderer, token: Remarkable.LinkOpenToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]",
|
||||
"link_close": () => "[/url]",
|
||||
|
||||
"image": (renderer: MD2BBCodeRenderer, token: Remarkable.ImageToken) => "[img=" + (token.src) + "]" + (token.alt || token.src) + "[/img]",
|
||||
|
||||
//footnote_ref
|
||||
|
||||
//"content": "==Marked text==",
|
||||
//mark_open
|
||||
//mark_close
|
||||
|
||||
//++Inserted text++
|
||||
"ins_open": () => "[u]",
|
||||
"ins_close": () => "[/u]",
|
||||
|
||||
"code": (renderer: MD2BBCodeRenderer, token: Remarkable.CodeToken) => "[i-code]" + escapeBBCode(token.content) + "[/i-code]",
|
||||
"fence": (renderer: MD2BBCodeRenderer, token: Remarkable.FenceToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + escapeBBCode(token.content) + "[/code]",
|
||||
|
||||
"heading_open": (renderer: MD2BBCodeRenderer, token: Remarkable.HeadingOpenToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]",
|
||||
"heading_close": () => "[/size][hr]",
|
||||
|
||||
"hr": () => "[hr]",
|
||||
|
||||
//> Experience real-time editing with Remarkable!
|
||||
"blockquote_open": () => "[quote]",
|
||||
"blockquote_close": () => "[/quote]"
|
||||
};
|
||||
|
||||
private _options;
|
||||
last_paragraph: Remarkable.Token;
|
||||
|
||||
render(tokens: Remarkable.Token[], options: Remarkable.Options, env: Remarkable.Env): string {
|
||||
this.last_paragraph = undefined;
|
||||
this._options = options;
|
||||
let result = '';
|
||||
|
||||
//TODO: Escape BB-Codes
|
||||
for(let index = 0; index < tokens.length; index++) {
|
||||
if (tokens[index].type === 'inline') {
|
||||
/* we're just ignoring the inline fact */
|
||||
result += this.render((tokens[index] as any).children, options, env);
|
||||
} else {
|
||||
result += this.renderToken(tokens[index], index);
|
||||
}
|
||||
}
|
||||
|
||||
this._options = undefined;
|
||||
return result;
|
||||
}
|
||||
|
||||
private renderToken(token: Remarkable.Token, index: number) {
|
||||
log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token);
|
||||
const renderer = MD2BBCodeRenderer.renderers[token.type];
|
||||
if(typeof(renderer) === "undefined") {
|
||||
log.warn(LogCategory.CHAT, tr("Missing markdown to bbcode renderer for token %s: %o"), token.type, token);
|
||||
return 'content' in token ? token.content : "";
|
||||
}
|
||||
|
||||
const result = renderer(this, token);
|
||||
if(token.type === "paragraph_open")
|
||||
this.last_paragraph = token;
|
||||
return result;
|
||||
}
|
||||
|
||||
options() : any {
|
||||
return this._options;
|
||||
}
|
||||
|
||||
maybe_escape_bb(text: string) {
|
||||
if(this._options.escape_bb)
|
||||
return escapeBBCode(text);
|
||||
return text;
|
||||
}
|
||||
}
|
||||
|
||||
const remarkableRenderer = new Remarkable("full", {
|
||||
typographer: true
|
||||
});
|
||||
remarkableRenderer.renderer = new MD2BBCodeRenderer() as any;
|
||||
remarkableRenderer.inline.ruler.disable([ 'newline', 'autolink' ]);
|
||||
|
||||
function process_markdown(message: string, options: {
|
||||
process_url?: boolean,
|
||||
escape_bb?: boolean
|
||||
}) : string {
|
||||
|
||||
remarkableRenderer.set({
|
||||
process_url: !!options.process_url,
|
||||
escape_bb: !!options.escape_bb
|
||||
} as any);
|
||||
|
||||
let result: string = remarkableRenderer.render(message);
|
||||
if(result.endsWith("\n"))
|
||||
result = result.substr(0, result.length - 1);
|
||||
return result;
|
||||
}
|
||||
|
||||
export function preprocess_chat_message(message: string) : string {
|
||||
const process_url = settings.static_global(Settings.KEY_CHAT_TAG_URLS);
|
||||
const parse_markdown = settings.static_global(Settings.KEY_CHAT_ENABLE_MARKDOWN);
|
||||
const escape_bb = !settings.static_global(Settings.KEY_CHAT_ENABLE_BBCODE);
|
||||
|
||||
if(parse_markdown)
|
||||
return process_markdown(message, {
|
||||
process_url: process_url,
|
||||
escape_bb: escape_bb
|
||||
});
|
||||
|
||||
if(escape_bb)
|
||||
message = escapeBBCode(message);
|
||||
return process_url ? process_urls(message) : message;
|
||||
}
|
||||
|
||||
export namespace date {
|
||||
export function same_day(a: number | Date, b: number | Date) {
|
||||
a = a instanceof Date ? a : new Date(a);
|
||||
|
|
|
@ -7,9 +7,6 @@ import * as image_preview from "../image_preview";
|
|||
import {format} from "tc-shared/ui/frames/side/chat_helper";
|
||||
import * as i18nc from "tc-shared/i18n/country";
|
||||
|
||||
declare function setInterval(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
|
||||
export class ClientInfo {
|
||||
readonly handle: Frame;
|
||||
private _html_tag: JQuery;
|
||||
|
|
|
@ -9,8 +9,6 @@ import {createErrorModal, createInputModal} from "tc-shared/ui/elements/Modal";
|
|||
import * as log from "tc-shared/log";
|
||||
import * as image_preview from "../image_preview";
|
||||
|
||||
declare function setTimeout(handler: TimerHandler, timeout?: number, ...arguments: any[]): number;
|
||||
|
||||
interface LoadedSongData {
|
||||
description: string;
|
||||
title: string;
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import {ConnectionHandler} from "tc-shared/ConnectionHandler";
|
||||
import {createModal, Modal} from "tc-shared/ui/elements/Modal";
|
||||
import * as htmltags from "tc-shared/ui/htmltags";
|
||||
import {bbcode_chat} from "tc-shared/ui/frames/chat";
|
||||
import * as moment from "moment";
|
||||
import {renderBBCodeAsJQuery} from "tc-shared/text/bbcode";
|
||||
|
||||
let global_modal: PokeModal;
|
||||
|
||||
|
@ -60,7 +60,7 @@ class PokeModal {
|
|||
}))).appendTo(container);
|
||||
if(message) {
|
||||
$.spawn("div").addClass("text").text(tr("pokes you:")).appendTo(container);
|
||||
$.spawn("div").addClass("poke-message").append(...bbcode_chat(message)).appendTo(container);
|
||||
$.spawn("div").addClass("poke-message").append(...renderBBCodeAsJQuery(message, { convertSingleUrls: false })).appendTo(container);
|
||||
} else {
|
||||
$.spawn("div").addClass("text").text(tr("pokes you.")).appendTo(container);
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue