import { describe, it, expect } from "vitest"; import MarkdownRenderer from "@/js/MarkdownRenderer"; describe("MarkdownRenderer.js", () => { describe("render (ConversationViewer / message bodies)", () => { it("escapes leading greater-than so blockquote regex does not apply (documented behaviour)", () => { const result = MarkdownRenderer.render("> quoted line\n\nNormal paragraph."); expect(result).toContain("> quoted line"); expect(result).toContain("Normal paragraph"); expect(result).not.toContain("blockquote"); }); }); describe("render", () => { it("renders basic text correctly", () => { expect(MarkdownRenderer.render("Hello")).toContain("Hello"); }); it("renders bold text correctly", () => { const result = MarkdownRenderer.render("**Bold**"); expect(result).toContain("Bold"); }); it("renders italic text correctly", () => { const result = MarkdownRenderer.render("*Italic*"); expect(result).toContain("Italic"); }); it("renders underscore italic when delimiters are word boundaries", () => { const result = MarkdownRenderer.render("this is _italic_ text"); expect(result).toContain("this is italic text"); }); it("renders bold and italic text correctly", () => { const result = MarkdownRenderer.render("***Bold and Italic***"); expect(result).toContain("Bold and Italic"); }); it("renders headers correctly", () => { expect(MarkdownRenderer.render("# Header 1")).toContain("
{
const url = "https://git.quad4.io/RNS-Things/MeshChatX/src/branch/dev/docs/meshchatx_on_raspberry_pi.md";
const result = MarkdownRenderer.render(`visit ${url}`);
expect(result).toContain(`href="${url}"`);
expect(result).toContain(url);
expect(result).not.toContain("on");
expect(result).not.toContain("raspberry");
});
it("renders fenced code blocks correctly", () => {
const result = MarkdownRenderer.render("```python\nprint('hello')\n```");
expect(result).toContain(" {
const result = MarkdownRenderer.render("Para 1\n\nPara 2");
expect(result).toContain(" {
const result = MarkdownRenderer.render("snake_case_identifier should remain plain");
expect(result).toContain("snake_case_identifier should remain plain");
expect(result).not.toContain("case");
});
it("keeps underscore-heavy urls intact while still rendering links", () => {
const url =
"https://example.com/docs/meshchatx_on_raspberry_pi.md?file=meshchatx_on_raspberry_pi.md#meshchatx_on_raspberry_pi";
const result = MarkdownRenderer.render(`see ${url} now`);
expect(result).toContain(`href="${url}"`);
expect(result).toContain(url);
expect(result).not.toContain("on");
expect(result).not.toContain("raspberry");
});
it("renders multiple urls in one line without corruption", () => {
const a = "https://example.com/docs/meshchatx_on_pi.md";
const b = "https://example.com/plain";
const result = MarkdownRenderer.render(`links: ${a} and ${b}`);
expect(result).toContain(`href="${a}"`);
expect(result).toContain(`href="${b}"`);
expect((result.match(/ {
const result = MarkdownRenderer.render("Check (https://example.com/path_(v1)), and continue.");
expect(result).toContain('href="https://example.com/path_(v1)"');
expect(result).toContain("), and continue.");
});
it("supports encoded chars and balanced parentheses in link path", () => {
const url = "https://example.com/docs/file%5Fname_(v1).md";
const result = MarkdownRenderer.render(`open ${url}`);
expect(result).toContain(`href="${url}"`);
});
it("keeps escaped entities in query string links", () => {
const url = "https://example.com/search?q=a&lang=en";
const result = MarkdownRenderer.render(`lookup ${url}`);
expect(result).toContain('href="https://example.com/search?q=a&lang=en"');
expect(result).toContain("https://example.com/search?q=a&lang=en");
});
it("handles links at line boundaries with newline conversion", () => {
const url = "https://example.com/meshchatx_on_pi.md";
const result = MarkdownRenderer.render(`${url}\nnext line`);
expect(result).toContain(`href="${url}"`);
expect(result).toContain("
next line");
});
it("mixes underscore markdown with underscore urls safely", () => {
const url = "https://example.com/meshchatx_on_raspberry_pi.md";
const result = MarkdownRenderer.render(`_label_ ${url} _tail_`);
expect(result).toContain("label");
expect(result).toContain("tail");
expect(result).toContain(`href="${url}"`);
expect(result).not.toContain("on");
});
it("escapes pre-rendered html input safely instead of nesting raw anchors", () => {
const preRendered = '
';
const result = MarkdownRenderer.render(preRendered);
expect(result).toContain("<p>");
expect(result).toContain("<a href="");
expect(result).not.toContain("";
const result = MarkdownRenderer.render(malformed);
expect(result).not.toContain("\n```";
const result = MarkdownRenderer.render(malformed);
expect(result).toContain("<script>");
});
it("escapes html in inline code", () => {
const malformed = "``";
const result = MarkdownRenderer.render(malformed);
expect(result).toContain("<script>");
});
});
describe("reticulum links", () => {
it("detects nomadnet:// links with hash and path", () => {
const text = "check this out: nomadnet://1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu";
const result = MarkdownRenderer.render(text);
expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"');
expect(result).toContain("nomadnet://1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu");
});
it("detects bare hash and path links as nomadnet", () => {
const text = "node is at 1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu";
const result = MarkdownRenderer.render(text);
expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"');
expect(result).toContain("1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu");
});
it("detects nomadnet:// links with just hash", () => {
const text = "nomadnet://1dfeb0d794963579bd21ac8f153c77a4";
const result = MarkdownRenderer.render(text);
expect(result).toContain('data-nomadnet-url="1dfeb0d794963579bd21ac8f153c77a4:/page/index.mu"');
});
it("does not detect bare hash as lxmf without prefix", () => {
const text = "send to 1dfeb0d794963579bd21ac8f153c77a4";
const result = MarkdownRenderer.render(text);
expect(result).not.toContain("lxmf-link");
expect(result).toContain("1dfeb0d794963579bd21ac8f153c77a4");
});
it("detects lxmf: prefixed hash as lxmf link", () => {
const text = "send to lxmf:1dfeb0d794963579bd21ac8f153c77a4";
const result = MarkdownRenderer.render(text);
expect(result).toContain('class="lxmf-link');
expect(result).toContain('data-lxmf-address="1dfeb0d794963579bd21ac8f153c77a4"');
});
it("does not detect invalid hashes", () => {
const text = "not-a-hash:/page/index.mu";
const result = MarkdownRenderer.render(text);
expect(result).not.toContain("nomadnet-link");
expect(result).not.toContain("lxmf-link");
});
});
describe("fuzzing: stability testing", () => {
const generateRandomString = (length) => {
const chars =
"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789!@#$%^&*()_+-=[]{}|;':\",./<>?`~ \n\r\t";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
};
it("handles random inputs without crashing (100 iterations)", () => {
for (let i = 0; i < 100; i++) {
const randomText = generateRandomString(Math.floor(Math.random() * 1000));
expect(() => {
MarkdownRenderer.render(randomText);
}).not.toThrow();
}
});
it("handles deeply nested or complex markdown patterns without crashing", () => {
const complex = "# ".repeat(100) + "**".repeat(100) + "```".repeat(100) + "```\n".repeat(10);
expect(() => {
MarkdownRenderer.render(complex);
}).not.toThrow();
});
it("handles large inputs correctly (1MB of random text)", () => {
const largeText = generateRandomString(1024 * 1024);
const start = Date.now();
const result = MarkdownRenderer.render(largeText);
const end = Date.now();
expect(typeof result).toBe("string");
// performance check: should be relatively fast (less than 500ms for 1MB usually)
expect(end - start).toBeLessThan(1000);
});
it("handles potential ReDoS patterns (repeated separators)", () => {
// Test patterns that often cause ReDoS in poorly written markdown parsers (can never be too careful, especially on public testnets)
const redosPatterns = [
"*".repeat(10000), // Long string of bold markers
"#".repeat(10000), // Long string of header markers
"`".repeat(10000), // Long string of backticks
" ".repeat(10000) + "\n", // Long string of whitespace
"[](".repeat(5000), // Unclosed links (if we added them)
"** ".repeat(5000), // Bold marker followed by space repeated
];
redosPatterns.forEach((pattern) => {
const start = Date.now();
MarkdownRenderer.render(pattern);
const end = Date.now();
expect(end - start).toBeLessThan(100); // Should be very fast
});
});
it("handles unicode homoglyphs and special characters without interference", () => {
const homoglyphs = [
"**bold**",
"∗∗notbold∗∗", // unicode asterisks
"# header",
"# not header", // fullwidth hash
"`code`",
"`notcode`", // fullwidth backtick
];
homoglyphs.forEach((text) => {
const result = MarkdownRenderer.render(text);
expect(typeof result).toBe("string");
});
});
it("fuzzing: full unicode code points do not crash render or strip", () => {
for (let i = 0; i < 200; i++) {
let s = "";
const len = Math.floor(Math.random() * 900);
for (let j = 0; j < len; j++) {
s += String.fromCharCode(Math.floor(Math.random() * 65536));
}
expect(() => MarkdownRenderer.render(s)).not.toThrow();
expect(() => MarkdownRenderer.strip(s)).not.toThrow();
const r = MarkdownRenderer.render(s);
expect(r.toLowerCase()).not.toContain("",
"javascript:alert(1)",
"data:text/html,",
"'';alert(1);//",
];
risky.forEach((s) => {
const r = MarkdownRenderer.render(s);
expect(r).not.toMatch(/ \ufffd";
const r = MarkdownRenderer.render(msg);
expect(r).not.toContain("