/** * Tests for inline HTML parsing */ import { describe, it, expect } from "vitest"; import { parseInlineContent, extractText, extractAlt, extractCaption, extractSrc, } from "../src/inline.js"; let keyCounter = 0; const generateKey = () => `key-${++keyCounter}`; const NEWLINE_PATTERN = /\n/g; describe("parseInlineContent", () => { describe("plain text", () => { it("parses plain text", () => { const result = parseInlineContent("Hello world", generateKey); expect(result.children).toHaveLength(1); expect(result.children[0]).toMatchObject({ _type: "span", text: "Hello world", }); expect(result.markDefs).toHaveLength(0); }); it("handles empty string", () => { const result = parseInlineContent("", generateKey); expect(result.children).toHaveLength(1); expect(result.children[0]).toMatchObject({ _type: "span", text: "", }); }); it("handles whitespace-only string", () => { const result = parseInlineContent(" ", generateKey); expect(result.children).toHaveLength(1); expect(result.children[0]?.text).toBe(" "); }); it("preserves newlines in text", () => { const result = parseInlineContent("line1\nline2", generateKey); // Should have one span with newline appended, then another span expect(result.children.length).toBeGreaterThanOrEqual(1); const fullText = result.children.map((c) => c.text).join(""); expect(fullText).toContain("line1"); expect(fullText).toContain("line2"); }); }); describe("basic formatting", () => { it("parses tags", () => { const result = parseInlineContent("Hello bold world", generateKey); expect(result.children).toHaveLength(3); expect(result.children[0]).toMatchObject({ text: "Hello " }); expect(result.children[1]).toMatchObject({ text: "bold", marks: ["strong"], }); expect(result.children[2]).toMatchObject({ text: " world" }); }); it("parses tags as strong", () => { const result = parseInlineContent("Hello bold world", generateKey); expect(result.children[1]).toMatchObject({ text: "bold", marks: ["strong"], }); }); it("parses tags", () => { const result = parseInlineContent("Hello italic world", generateKey); expect(result.children[1]).toMatchObject({ text: "italic", marks: ["em"], }); }); it("parses tags as em", () => { const result = parseInlineContent("Hello italic world", generateKey); expect(result.children[1]).toMatchObject({ text: "italic", marks: ["em"], }); }); it("parses tags", () => { const result = parseInlineContent("Hello underline world", generateKey); expect(result.children[1]).toMatchObject({ text: "underline", marks: ["underline"], }); }); it("parses tags as strike-through", () => { const result = parseInlineContent("Hello strikethrough world", generateKey); expect(result.children[1]).toMatchObject({ text: "strikethrough", marks: ["strike-through"], }); }); it("parses tags as strike-through", () => { const result = parseInlineContent("Hello deleted world", generateKey); expect(result.children[1]).toMatchObject({ text: "deleted", marks: ["strike-through"], }); }); it("parses tags", () => { const result = parseInlineContent("Use const x = 1 for variables", generateKey); expect(result.children[1]).toMatchObject({ text: "const x = 1", marks: ["code"], }); }); it("parses tags", () => { const result = parseInlineContent("x2", generateKey); expect(result.children[1]).toMatchObject({ text: "2", marks: ["superscript"], }); }); it("parses tags", () => { const result = parseInlineContent("H2O", generateKey); expect(result.children[1]).toMatchObject({ text: "2", marks: ["subscript"], }); }); }); describe("nested formatting", () => { it("handles nested strong and em", () => { const result = parseInlineContent("bold italic", generateKey); expect(result.children).toHaveLength(1); expect(result.children[0]).toMatchObject({ text: "bold italic", marks: expect.arrayContaining(["strong", "em"]), }); }); it("handles deeply nested marks", () => { const result = parseInlineContent("code", generateKey); expect(result.children[0]?.marks).toContain("strong"); expect(result.children[0]?.marks).toContain("em"); expect(result.children[0]?.marks).toContain("code"); }); it("handles mixed content with nested marks", () => { const result = parseInlineContent( "Start bold bold-italic bold end", generateKey, ); expect(result.children.length).toBeGreaterThanOrEqual(4); // Find the bold-italic span const boldItalic = result.children.find( (c) => c.marks?.includes("strong") && c.marks?.includes("em"), ); expect(boldItalic?.text).toBe("bold-italic"); }); }); describe("links", () => { it("parses simple links", () => { const result = parseInlineContent( 'Visit our site', generateKey, ); expect(result.markDefs).toHaveLength(1); expect(result.markDefs[0]).toMatchObject({ _type: "link", href: "https://example.com", }); const linkSpan = result.children.find((c) => c.marks?.includes(result.markDefs[0]?._key ?? ""), ); expect(linkSpan?.text).toBe("our site"); }); it("handles links with target=_blank", () => { const result = parseInlineContent( 'link', generateKey, ); expect(result.markDefs[0]).toMatchObject({ _type: "link", href: "https://example.com", blank: true, }); }); it("deduplicates identical links", () => { const result = parseInlineContent( 'link1 and link2', generateKey, ); expect(result.markDefs).toHaveLength(1); const linkKey = result.markDefs[0]?._key; const linkSpans = result.children.filter((c) => c.marks?.includes(linkKey ?? "")); expect(linkSpans).toHaveLength(2); }); it("creates separate markDefs for different links", () => { const result = parseInlineContent( 'link1 and link2', generateKey, ); expect(result.markDefs).toHaveLength(2); expect(result.markDefs.map((m) => m.href)).toContain("https://a.com"); expect(result.markDefs.map((m) => m.href)).toContain("https://b.com"); }); it("handles links with formatting inside", () => { const result = parseInlineContent( 'bold link', generateKey, ); const span = result.children.find((c) => c.text === "bold link"); expect(span?.marks).toContain("strong"); expect(span?.marks?.length).toBe(2); // strong + link key }); it("handles links with empty href", () => { const result = parseInlineContent('empty link', generateKey); expect(result.markDefs).toHaveLength(1); expect(result.markDefs[0]).toMatchObject({ _type: "link", href: "", }); const linkSpan = result.children.find((c) => c.marks?.includes(result.markDefs[0]?._key ?? ""), ); expect(linkSpan?.text).toBe("empty link"); }); it("ignores unknown schemes in links", () => { const result = parseInlineContent('bad link', generateKey); expect(result.markDefs).toHaveLength(1); expect(result.markDefs[0]).toMatchObject({ _type: "link", href: "", }); const linkSpan = result.children.find((c) => c.marks?.includes(result.markDefs[0]!._key)); expect(linkSpan?.text).toBe("bad link"); }); }); describe("line breaks", () => { it("handles
tags", () => { const result = parseInlineContent("line1
line2", generateKey); const fullText = result.children.map((c) => c.text).join(""); expect(fullText).toContain("line1"); expect(fullText).toContain("\n"); expect(fullText).toContain("line2"); }); it("handles self-closing
tags", () => { const result = parseInlineContent("line1
line2", generateKey); const fullText = result.children.map((c) => c.text).join(""); expect(fullText).toContain("\n"); }); it("handles multiple consecutive
tags", () => { const result = parseInlineContent("a

b", generateKey); const fullText = result.children.map((c) => c.text).join(""); expect(fullText.match(NEWLINE_PATTERN)?.length).toBeGreaterThanOrEqual(2); }); }); describe("block wrapper stripping", () => { it("strips

wrapper", () => { const result = parseInlineContent("

content

", generateKey); expect(result.children).toHaveLength(1); expect(result.children[0]?.text).toBe("content"); }); it("strips heading wrappers", () => { const result = parseInlineContent("

heading

", generateKey); expect(result.children[0]?.text).toBe("heading"); }); it("strips
  • wrapper", () => { const result = parseInlineContent("
  • list item
  • ", generateKey); expect(result.children[0]?.text).toBe("list item"); }); it("preserves content when wrapper has attributes", () => { const result = parseInlineContent('

    content

    ', generateKey); expect(result.children[0]?.text).toBe("content"); }); }); }); describe("extractText", () => { it("extracts plain text", () => { expect(extractText("Hello world")).toBe("Hello world"); }); it("strips HTML tags", () => { expect(extractText("

    Hello world

    ")).toBe("Hello world"); }); it("handles nested elements", () => { expect(extractText("

    Nested text

    ")).toBe("Nested text"); }); it("handles empty string", () => { expect(extractText("")).toBe(""); }); }); describe("extractAlt", () => { it("extracts alt from img tag", () => { expect(extractAlt('A photo')).toBe("A photo"); }); it("handles missing alt", () => { expect(extractAlt('')).toBeUndefined(); }); it("handles empty alt", () => { expect(extractAlt('')).toBe(""); }); it("handles single quotes", () => { expect(extractAlt("A photo")).toBe("A photo"); }); }); describe("extractCaption", () => { it("extracts caption from figcaption", () => { expect(extractCaption("
    My caption
    ")).toBe( "My caption", ); }); it("strips HTML from caption", () => { expect( extractCaption("
    Caption with formatting
    "), ).toBe("Caption with formatting"); }); it("handles missing figcaption", () => { expect(extractCaption("
    ")).toBeUndefined(); }); }); describe("extractSrc", () => { it("extracts src from img tag", () => { expect(extractSrc('')).toBe( "https://example.com/photo.jpg", ); }); it("handles relative URLs", () => { expect(extractSrc('')).toBe("/uploads/photo.jpg"); }); it("handles missing src", () => { expect(extractSrc("no source")).toBeUndefined(); }); });