diff --git a/electron/main.js b/electron/main.js index 4cdd531..59b8cc3 100644 --- a/electron/main.js +++ b/electron/main.js @@ -348,9 +348,9 @@ app.whenReady().then(async () => { "default-src 'self'", "script-src 'self' 'unsafe-inline' 'unsafe-eval'", "style-src 'self' 'unsafe-inline'", - "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org", + "img-src 'self' data: blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://*.cartocdn.com", "font-src 'self' data:", - "connect-src 'self' http://localhost:9337 https://localhost:9337 ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org https://git.quad4.io", + "connect-src 'self' http://localhost:9337 https://localhost:9337 ws://localhost:* wss://localhost:* blob: https://*.tile.openstreetmap.org https://tile.openstreetmap.org https://nominatim.openstreetmap.org https://git.quad4.io https://*.cartocdn.com", "media-src 'self' blob:", "worker-src 'self' blob:", "frame-src 'self'", diff --git a/meshchatx/src/frontend/js/LinkUtils.js b/meshchatx/src/frontend/js/LinkUtils.js new file mode 100644 index 0000000..c19a80c --- /dev/null +++ b/meshchatx/src/frontend/js/LinkUtils.js @@ -0,0 +1,41 @@ +export default class LinkUtils { + /** + * Detects and wraps NomadNet links in HTML. + * Supports nomadnet://:/path and :/path + */ + static renderNomadNetLinks(text) { + if (!text) return ""; + + // Hash is 32 hex chars. Path is optional. + const hashPattern = "[a-fA-F0-9]{32}"; + const nomadnetRegex = new RegExp(`(?:nomadnet://)?(${hashPattern})(?::(/[\\w\\d./?%&=-]*))?`, "g"); + + return text.replace(nomadnetRegex, (match, hash, path) => { + const fullPath = path || "/page/index.mu"; + const url = `${hash}:${fullPath}`; + return `${match}`; + }); + } + + /** + * Basic URL detection for standard http/https links. + */ + static renderStandardLinks(text) { + if (!text) return ""; + + // Simple regex for URLs + const urlRegex = /(https?:\/\/[^\s<]+)/g; + return text.replace(urlRegex, (url) => { + return `${url}`; + }); + } + + /** + * Applies all link rendering. + */ + static renderAllLinks(text) { + text = this.renderStandardLinks(text); + text = this.renderNomadNetLinks(text); + return text; + } +} diff --git a/meshchatx/src/frontend/js/MarkdownRenderer.js b/meshchatx/src/frontend/js/MarkdownRenderer.js new file mode 100644 index 0000000..8da04b8 --- /dev/null +++ b/meshchatx/src/frontend/js/MarkdownRenderer.js @@ -0,0 +1,101 @@ +import Utils from "./Utils"; +import LinkUtils from "./LinkUtils"; + +export default class MarkdownRenderer { + /** + * A simple Markdown to HTML renderer, cause we dont need another library for this. + * Ported and simplified from meshchatx/src/backend/markdown_renderer.py + */ + static render(text) { + if (!text) { + return ""; + } + + // Escape HTML entities first to prevent XSS + text = Utils.escapeHtml(text); + + // Fenced code blocks - process these FIRST and replace with placeholders + const code_blocks = []; + text = text.replace(/```(\w+)?\n([\s\S]*?)\n```/g, (match, lang, code) => { + const placeholder = `[[CB${code_blocks.length}]]`; + code_blocks.push( + `
${code}
` + ); + return placeholder; + }); + + // Headers + text = text.replace(/^# (.*)$/gm, '

$1

'); + text = text.replace(/^## (.*)$/gm, '

$1

'); + text = text.replace(/^### (.*)$/gm, '

$1

'); + + // Bold and Italic + text = text.replace(/\*\*\*(.*?)\*\*\*/g, "$1"); + text = text.replace(/\*\*(.*?)\*\*/g, "$1"); + text = text.replace(/\*(.*?)\*/g, "$1"); + text = text.replace(/___(.*?)___/g, "$1"); + text = text.replace(/__(.*?)__/g, "$1"); + text = text.replace(/_(.*?)_/g, "$1"); + + // Inline code + text = text.replace( + /`([^`]+)`/g, + '$1' + ); + + // Links + text = LinkUtils.renderAllLinks(text); + + // Restore code blocks + for (let i = 0; i < code_blocks.length; i++) { + text = text.replace(`[[CB${i}]]`, code_blocks[i]); + } + + // Paragraphs - double newline to p tag + const parts = text.split(/\n\n+/); + const processed_parts = []; + for (let part of parts) { + part = part.trim(); + if (!part) continue; + + // If it's a placeholder for code block, don't wrap in

+ if (part.startsWith(" for line breaks within paragraphs + part = part.replace(/\n/g, "
"); + processed_parts.push(`

${part}

`); + } + } + + return processed_parts.join("\n"); + } + + /** + * Strips markdown from text for previews. + */ + static strip(text) { + if (!text) { + return ""; + } + + // Strip fenced code blocks + text = text.replace(/```(\w+)?\n([\s\S]*?)\n```/g, "[Code Block]"); + + // Strip headers + text = text.replace(/^#+ (.*)$/gm, "$1"); + + // Strip bold and italic + text = text.replace(/\*\*\*(.*?)\*\*\*/g, "$1"); + text = text.replace(/\*\*(.*?)\*\*/g, "$1"); + text = text.replace(/\*(.*?)\*/g, "$1"); + text = text.replace(/___(.*?)___/g, "$1"); + text = text.replace(/__(.*?)__/g, "$1"); + text = text.replace(/_(.*?)_/g, "$1"); + + // Strip inline code + text = text.replace(/`([^`]+)`/g, "$1"); + + return text; + } +} diff --git a/meshchatx/src/frontend/js/ToastUtils.js b/meshchatx/src/frontend/js/ToastUtils.js index 600fb32..ca328d0 100644 --- a/meshchatx/src/frontend/js/ToastUtils.js +++ b/meshchatx/src/frontend/js/ToastUtils.js @@ -1,24 +1,28 @@ import GlobalEmitter from "./GlobalEmitter"; class ToastUtils { - static show(message, type = "info", duration = 5000) { - GlobalEmitter.emit("toast", { message, type, duration }); + static show(message, type = "info", duration = 5000, key = null) { + GlobalEmitter.emit("toast", { message, type, duration, key }); } - static success(message, duration = 5000) { - this.show(message, "success", duration); + static success(message, duration = 5000, key = null) { + this.show(message, "success", duration, key); } - static error(message, duration = 5000) { - this.show(message, "error", duration); + static error(message, duration = 5000, key = null) { + this.show(message, "error", duration, key); } - static warning(message, duration = 5000) { - this.show(message, "warning", duration); + static warning(message, duration = 5000, key = null) { + this.show(message, "warning", duration, key); } - static info(message, duration = 5000) { - this.show(message, "info", duration); + static info(message, duration = 5000, key = null) { + this.show(message, "info", duration, key); + } + + static loading(message, duration = 0, key = null) { + this.show(message, "loading", duration, key); } } diff --git a/meshchatx/src/frontend/js/Utils.js b/meshchatx/src/frontend/js/Utils.js index 125939b..9dfb99c 100644 --- a/meshchatx/src/frontend/js/Utils.js +++ b/meshchatx/src/frontend/js/Utils.js @@ -183,6 +183,20 @@ class Utils { const value = rawValue?.toString()?.toLowerCase(); return value === "on" || value === "yes" || value === "true"; } + + static escapeHtml(text) { + if (!text) return ""; + const map = { + "&": "&", + "<": "<", + ">": ">", + '"': """, + "'": "'", + }; + return text.replace(/[&<>"']/g, function (m) { + return map[m]; + }); + } } export default Utils; diff --git a/meshchatx/src/frontend/main.js b/meshchatx/src/frontend/main.js index b4dcf8c..9e60072 100644 --- a/meshchatx/src/frontend/main.js +++ b/meshchatx/src/frontend/main.js @@ -12,6 +12,7 @@ import App from "./components/App.vue"; import en from "./locales/en.json"; import de from "./locales/de.json"; import ru from "./locales/ru.json"; +import it from "./locales/it.json"; // init i18n const i18n = createI18n({ @@ -22,6 +23,7 @@ const i18n = createI18n({ en, de, ru, + it, }, }); @@ -179,6 +181,11 @@ const router = createRouter({ path: "/rnpath", component: defineAsyncComponent(() => import("./components/tools/RNPathPage.vue")), }, + { + name: "rnpath-trace", + path: "/rnpath-trace", + component: defineAsyncComponent(() => import("./components/tools/RNPathTracePage.vue")), + }, { name: "rnprobe", path: "/rnprobe",