356 lines
No EOL
14 KiB
TypeScript
356 lines
No EOL
14 KiB
TypeScript
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);
|
|
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
|
|
}
|
|
|
|
function dateEqual(a: Date, b: Date) {
|
|
return a.getUTCFullYear() === b.getUTCFullYear() &&
|
|
a.getUTCMonth() === b.getUTCMonth() &&
|
|
a.getUTCDate() === b.getUTCDate();
|
|
}
|
|
|
|
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;
|
|
|
|
if(dateEqual(date, now))
|
|
return ColloquialFormat.TODAY;
|
|
|
|
date = new Date(date.getTime());
|
|
date.setDate(date.getDate() + 1);
|
|
|
|
if(dateEqual(date, now))
|
|
return ColloquialFormat.YESTERDAY;
|
|
|
|
return ColloquialFormat.GENERAL;
|
|
}
|
|
|
|
export function formatDayTime(date: Date) {
|
|
return ("0" + date.getHours()).substr(-2) + ":" + ("0" + date.getMinutes()).substr(-2);
|
|
}
|
|
|
|
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")) + " " + ("0" + hrs).substr(-2) + ":" + ("0" + date.getMinutes()).substr(-2) + " " + 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);
|
|
}
|
|
}
|
|
} |