TeaWeb/shared/js/ui/frames/side/chat_helper.ts
2020-03-30 13:44:18 +02:00

426 lines
No EOL
16 KiB
TypeScript

import * as log from "tc-shared/log";
import {LogCategory} from "tc-shared/log";
import {Settings, settings} from "tc-shared/settings";
declare const xbbcode;
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(" ");
}
namespace md2bbc {
export type RemarkToken = {
type: string;
tight: boolean;
lines: number[];
level: number;
/* img */
alt?: string;
src?: string;
/* link */
href?: string;
/* table */
align?: string;
/* code */
params?: string;
content?: string;
hLevel?: number;
children?: RemarkToken[];
}
export class Renderer {
private static renderers = {
"text": (renderer: Renderer, token: RemarkToken) => 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: Renderer, token: RemarkToken) => {
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(e => "[br]").join("");
},
"paragraph_close": () => "",
"strong_open": (renderer: Renderer, token: RemarkToken) => "[b]",
"strong_close": (renderer: Renderer, token: RemarkToken) => "[/b]",
"em_open": (renderer: Renderer, token: RemarkToken) => "[i]",
"em_close": (renderer: Renderer, token: RemarkToken) => "[/i]",
"del_open": () => "[s]",
"del_close": () => "[/s]",
"sup": (renderer: Renderer, token: RemarkToken) => "[sup]" + renderer.maybe_escape_bb(token.content) + "[/sup]",
"sub": (renderer: Renderer, token: RemarkToken) => "[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: Renderer, token: RemarkToken) => "[th" + (token.align ? ("=" + token.align) : "") + "]",
"th_close": () => "[/th]",
"td_open": () => "[td]",
"td_close": () => "[/td]",
"link_open": (renderer: Renderer, token: RemarkToken) => "[url" + (token.href ? ("=" + token.href) : "") + "]",
"link_close": () => "[/url]",
"image": (renderer: Renderer, token: RemarkToken) => "[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]",
/*
```
test
[/code]
test
```
*/
"code": (renderer: Renderer, token: RemarkToken) => "[i-code]" + xbbcode.escape(token.content) + "[/i-code]",
"fence": (renderer: Renderer, token: RemarkToken) => "[code" + (token.params ? ("=" + token.params) : "") + "]" + xbbcode.escape(token.content) + "[/code]",
"heading_open": (renderer: Renderer, token: RemarkToken) => "[size=" + (9 - Math.min(4, token.hLevel)) + "]",
"heading_close": (renderer: Renderer, token: RemarkToken) => "[/size][hr]",
"hr": () => "[hr]",
//> Experience real-time editing with Remarkable!
//blockquote_open,
//blockquote_close
};
private _options;
last_paragraph: RemarkToken;
render(tokens: RemarkToken[], options: any, env: any) {
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') {
result += this.render_inline(tokens[index].children, index);
} else {
result += this.render_token(tokens[index], index);
}
}
this._options = undefined;
return result;
}
private render_token(token: RemarkToken, index: number) {
log.debug(LogCategory.GENERAL, tr("Render Markdown token: %o"), token);
const renderer = Renderer.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 token.content || "";
}
const result = renderer(this, token, index);
if(token.type === "paragraph_open") this.last_paragraph = token;
return result;
}
private render_inline(tokens: RemarkToken[], index: number) {
let result = '';
for(let index = 0; index < tokens.length; index++) {
result += this.render_token(tokens[index], index);
}
return result;
}
options() : any {
return this._options;
}
maybe_escape_bb(text: string) {
if(this._options.escape_bb)
return xbbcode.escape(text);
return text;
}
}
}
let _renderer: any;
function process_markdown(message: string, options: {
process_url?: boolean,
escape_bb?: boolean
}) : string {
if(typeof(window.remarkable) === "undefined")
return (options.process_url ? process_urls(message) : message);
if(!_renderer) {
_renderer = new window.remarkable.Remarkable('full');
_renderer.set({
typographer: true
});
_renderer.renderer = new md2bbc.Renderer();
_renderer.inline.ruler.disable([ 'newline', 'autolink' ]);
}
_renderer.set({
process_url: !!options.process_url,
escape_bb: !!options.escape_bb
});
let result: string = _renderer.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 = xbbcode.escape(message);
return process_url ? process_urls(message) : message;
}
export namespace history {
let _local_cache: Cache;
async function get_cache() {
if(_local_cache)
return _local_cache;
if(!('caches' in window))
throw "missing cache extension!";
return (_local_cache = await caches.open('chat_history'));
}
export async function load_history(key: string) : Promise<any | undefined> {
const cache = await get_cache();
const request = new Request("https://_local_cache/cache_request_" + key);
const cached_response = await cache.match(request);
if(!cached_response)
return undefined;
return await cached_response.json();
}
export async function save_history(key: string, value: any) {
const cache = await get_cache();
const request = new Request("https://_local_cache/cache_request_" + key);
const data = JSON.stringify(value);
const new_headers = new Headers();
new_headers.set("Content-type", "application/json");
new_headers.set("Content-length", data.length.toString());
await cache.put(request, new Response(data, {
headers: new_headers
}));
}
}
export namespace date {
export function same_day(a: number | Date, b: number | Date) {
a = a instanceof Date ? a : new Date(a);
b = b instanceof Date ? b : new Date(b);
if(a.getDate() !== b.getDate())
return false;
if(a.getMonth() !== b.getMonth())
return false;
return a.getFullYear() === b.getFullYear();
}
}
}
export namespace format {
export namespace date {
export enum ColloquialFormat {
YESTERDAY,
TODAY,
GENERAL
}
export function date_format(date: Date, now: Date, ignore_settings?: boolean) : ColloquialFormat {
if(!ignore_settings && !settings.static_global(Settings.KEY_CHAT_COLLOQUIAL_TIMESTAMPS))
return ColloquialFormat.GENERAL;
let delta_day = now.getDate() - date.getDate();
if(delta_day < 1) /* month change? */
delta_day = date.getDate() - now.getDate();
if(delta_day == 0)
return ColloquialFormat.TODAY;
else if(delta_day == 1)
return ColloquialFormat.YESTERDAY;
return ColloquialFormat.GENERAL;
}
export function format_date_general(date: Date, hours?: boolean) : string {
return ('00' + date.getDate()).substr(-2) + "."
+ ('00' + date.getMonth()).substr(-2) + "."
+ date.getFullYear() +
(typeof(hours) === "undefined" || hours ? " at "
+ ('00' + date.getHours()).substr(-2) + ":"
+ ('00' + date.getMinutes()).substr(-2)
: "");
}
export function format_date_colloquial(date: Date, current_timestamp: Date) : { result: string; format: ColloquialFormat } {
const format = date_format(date, current_timestamp);
if(format == ColloquialFormat.GENERAL) {
return {
result: format_date_general(date),
format: format
};
} else {
let hrs = date.getHours();
let time = "AM";
if(hrs > 12) {
hrs -= 12;
time = "PM";
}
return {
result: (format == ColloquialFormat.YESTERDAY ? tr("Yesterday at") : tr("Today at")) + " " + hrs + ":" + date.getMinutes() + " " + time,
format: format
};
}
}
export function format_chat_time(date: Date) : {
result: string,
next_update: number /* in MS */
} {
const timestamp = date.getTime();
const current_timestamp = new Date();
const result = {
result: "",
next_update: 0
};
if(settings.static_global(Settings.KEY_CHAT_FIXED_TIMESTAMPS)) {
const format = format_date_colloquial(date, current_timestamp);
result.result = format.result;
result.next_update = 0; /* TODO: Update on day change? */
} else {
const delta = current_timestamp.getTime() - timestamp;
if(delta < 2000) {
result.result = "now";
result.next_update = 2500 - delta; /* update after two seconds */
} else if(delta < 30000) { /* 30 seconds */
result.result = Math.floor(delta / 1000) + " " + tr("seconds ago");
result.next_update = 1000; /* update every second */
} else if(delta < 30 * 60 * 1000) { /* 30 minutes */
if(delta < 120 * 1000)
result.result = tr("one minute ago");
else
result.result = Math.floor(delta / (1000 * 60)) + " " + tr("minutes ago");
result.next_update = 60000; /* updater after a minute */
} else {
result.result = format_date_colloquial(date, current_timestamp).result;
result.next_update = 0; /* TODO: Update on day change? */
}
}
return result;
}
}
export namespace time {
export function format_online_time(secs: number) : string {
let years = Math.floor(secs / (60 * 60 * 24 * 365));
let days = Math.floor(secs / (60 * 60 * 24)) % 365;
let hours = Math.floor(secs / (60 * 60)) % 24;
let minutes = Math.floor(secs / 60) % 60;
let seconds = Math.floor(secs % 60);
let result = "";
if(years > 0)
result += years + " " + tr("years") + " ";
if(years > 0 || days > 0)
result += days + " " + tr("days") + " ";
if(years > 0 || days > 0 || hours > 0)
result += hours + " " + tr("hours") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0)
result += minutes + " " + tr("minutes") + " ";
if(years > 0 || days > 0 || hours > 0 || minutes > 0 || seconds > 0)
result += seconds + " " + tr("seconds") + " ";
else
result = tr("now") + " ";
return result.substr(0, result.length - 1);
}
}
}