import { describe, expect, it } from "vitest"; import { htmlToMarkdown, renderNoteHtml, stripNote } from "./markdown"; // The sanitizer pipeline is the notes feature's XSS defense: markdown-it // with html disabled, then DOMPurify with a restrictive allow-list. These // tests pin the security-relevant behavior — loosening any of them should // require a deliberate decision, never an accident. describe("renderNoteHtml", () => { it("renders basic markdown to html", () => { const out = renderNoteHtml("**bold** and _ital_"); expect(out).toContain("bold"); expect(out).toContain("ital"); }); it("returns empty string for empty source", () => { expect(renderNoteHtml("")).toBe(""); }); it("strips raw script tags (parser-level html disabled)", () => { const out = renderNoteHtml("hello"); expect(out).not.toContain(" { const out = renderNoteHtml( '![x](y.png)\n\n
\n\n\n\n', ); expect(out).not.toContain(" { // html:false makes markdown-it ESCAPE raw html — the output string // contains the tags as visible text, which is inert wherever the // compact rows render it. const out = renderNoteHtml('text'); expect(out).not.toContain(" { // markdown-it's validateLink rejects the URL entirely — the raw syntax // stays as inert literal text and no element ever reaches the // sanitizer (which would then scheme-check it a second time). const out = renderNoteHtml("[click](javascript:alert(1))"); expect(out).not.toContain(" { const out = renderNoteHtml("![x](y.png)"); expect(out).not.toContain(" { const out = renderNoteHtml("[site](https://example.com)"); expect(out).toContain('href="https://example.com"'); }); it("renders footnotes", () => { const out = renderNoteHtml("note[^1]\n\n[^1]: the footnote"); expect(out).toContain("footnote-ref"); expect(out).toContain("the footnote"); }); }); describe("stripNote", () => { it("extracts plain text from markdown", () => { expect(stripNote("**bold** plain")).toBe("bold plain"); }); it("collapses whitespace and trims", () => { expect(stripNote("a\n\n b ")).toBe("a b"); }); it("reduces raw html to literal text (safe: html disabled, so tags never become elements)", () => { // markdown-it escapes the tags and DOMPurify leaves escaped entities // alone — the result is inert visible text, not executable markup. expect(stripNote("safe")).toBe("safe"); }); it("returns empty string for empty source", () => { expect(stripNote("")).toBe(""); }); }); describe("htmlToMarkdown", () => { it("converts emphasis", () => { expect(htmlToMarkdown("b")).toBe("**b**"); }); it("converts atx headings", () => { expect(htmlToMarkdown("

Title

")).toBe("## Title"); }); it("blanks images entirely", () => { expect(htmlToMarkdown('

ab

')).toBe("ab"); }); it("converts tables with heading rows to pipe syntax", () => { const md = htmlToMarkdown( "
AB
12
", ); expect(md).toContain("| A | B |"); expect(md).toContain("| 1 | 2 |"); }); it("converts headingless tables to blank-header pipes (never raw html)", () => { const md = htmlToMarkdown("
xy
"); expect(md).toContain("| x | y |"); expect(md).not.toContain("