import {Settings, settings} from "tc-shared/settings"; import * as contextmenu from "tc-shared/ui/elements/ContextMenu"; import {copy_to_clipboard} from "tc-shared/utils/helpers"; import {guid} from "tc-shared/crypto/uid"; import * as loader from "tc-loader"; import * as image_preview from "./ui/frames/image_preview" import * as DOMPurify from "dompurify"; declare const xbbcode; export namespace bbcode { const sanitizer_escaped = (key: string) => "[-- sescaped: " + key + " --]"; const sanitizer_escaped_regex = /\[-- sescaped: ([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}) --]/; const sanitizer_escaped_map: {[key: string]: string} = {}; const yt_url_regex = /^((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)(\S+)?$/; export interface FormatSettings { is_chat_message?: boolean } export function format(message: string, fsettings?: FormatSettings) : JQuery[] { 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 format("[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 format("[img]" + message + "[/img]"); } } const result = xbbcode.parse(message, { tag_whitelist: [ "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" ] }); let html = result.build_html(); if(typeof(window.twemoji) !== "undefined" && settings.static_global(Settings.KEY_CHAT_COLORED_EMOJIES)) html = twemoji.parse(html); const container = $.spawn("div"); let sanitized = DOMPurify.sanitize(html, { ADD_ATTR: [ "x-highlight-type", "x-code-type", "x-image-url" ] }); sanitized = sanitized.replace(sanitizer_escaped_regex, data => { const uid = data.match(sanitizer_escaped_regex)[1]; const value = sanitizer_escaped_map[uid]; if(!value) return data; delete sanitizer_escaped_map[uid]; return value; }); container[0].innerHTML = sanitized; container.find("a") .attr('target', "_blank") .on('contextmenu', event => { if(event.isDefaultPrevented()) return; event.preventDefault(); const url = $(event.target).attr("href"); contextmenu.spawn_context_menu(event.pageX, event.pageY, { callback: () => { const win = window.open(url, '_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(url), name: tr("Copy URL to clipboard"), type: contextmenu.MenuEntryType.ENTRY, icon_class: "client-copy" }); }); 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)); } export function load_image(entry: HTMLImageElement) { const url = decodeURIComponent(entry.getAttribute("x-image-url") || ""); const proxy_url = "https://images.weserv.nl/?url=" + encodeURIComponent(url); entry.onload = undefined; entry.src = proxy_url; 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', event => image_preview.preview_image(proxy_url, url)); } loader.register_task(loader.Stage.JAVASCRIPT_INITIALIZING, { name: "XBBCode code tag init", function: async () => { /* override default parser */ xbbcode.register.register_parser({ tag: ["code", "icode", "i-code"], content_tags_whitelist: [], build_html(layer) : string { const klass = layer.tag_normalized != 'code' ? "tag-hljs-inline-code" : "tag-hljs-code"; const language = (layer.options || "").replace("\"", "'").toLowerCase(); /* remove heading empty lines */ let text = layer.content.map(e => e.build_text()) .reduce((a, b) => a.length == 0 && b.replace(/[ \n\r\t]+/g, "").length == 0 ? "" : a + b, "") .replace(/^([ \n\r\t]*)(?=\n)+/g, ""); if(text.startsWith("\r") || text.startsWith("\n")) text = text.substr(1); let result: HighlightJSResult; if(window.hljs.getLanguage(language)) result = window.hljs.highlight(language, text, true); else result = window.hljs.highlightAuto(text); let html = '
';
                    html += '';
                    html += result.value;
                    return html + "
"; } }); /* override the yt parser */ const original_parser = xbbcode.register.find_parser("yt"); if(original_parser) xbbcode.register.register_parser({ tag: ["yt", "youtube"], build_html(layer): string { const result = original_parser.build_html(layer); if(!result.startsWith(""; return sanitizer_escaped(uid); } }); const load_callback = guid(); /* the image parse & displayer */ xbbcode.register.register_parser({ tag: ["img", "image"], build_html(layer): string { const uid = guid(); const fallback_value = "[img]" + layer.build_text() + "[/img]"; let target; let content = layer.content.map(e => e.build_text()).join(""); if (!layer.options) { target = content; } else target = layer.options; let url: URL; try { url = new URL(target); if(!url.hostname) throw ""; } catch(error) { return fallback_value; } sanitizer_escaped_map[uid] = "
"; return sanitizer_escaped(uid); } }); window[load_callback] = load_image; }, priority: 10 }); } export function sanitize_text(text: string) : string { return $(DOMPurify.sanitize("" + text + "", { ADD_ATTR: [ "x-highlight-type", "x-code-type", "x-image-url" ] })).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; 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); }