/** * Tests for the main Gutenberg to Portable Text converter */ import { describe, it, expect } from "vitest"; import { gutenbergToPortableText, htmlToPortableText, parseGutenbergBlocks } from "../src/index.js"; import type { PortableTextTextBlock, PortableTextImageBlock } from "../src/types.js"; const HTML_TAG_PATTERN = /<[^>]+>/g; describe("gutenbergToPortableText", () => { describe("empty content", () => { it("returns empty array for empty string", () => { expect(gutenbergToPortableText("")).toEqual([]); }); it("returns empty array for whitespace", () => { expect(gutenbergToPortableText(" \n\t ")).toEqual([]); }); it("returns empty array for null-ish values", () => { expect(gutenbergToPortableText(null as unknown as string)).toEqual([]); expect(gutenbergToPortableText(undefined as unknown as string)).toEqual([]); }); }); describe("paragraph blocks", () => { it("converts simple paragraph", () => { const content = `
Hello world
`; const result = gutenbergToPortableText(content); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ _type: "block", style: "normal", }); const block = result[0] as PortableTextTextBlock; expect(block.children[0]?.text).toBe("Hello world"); }); it("converts paragraph with inline formatting", () => { const content = `Hello bold and italic world
`; const result = gutenbergToPortableText(content); const block = result[0] as PortableTextTextBlock; expect(block.children.length).toBeGreaterThan(1); const boldSpan = block.children.find((c) => c.marks?.includes("strong")); const italicSpan = block.children.find((c) => c.marks?.includes("em")); expect(boldSpan?.text).toBe("bold"); expect(italicSpan?.text).toBe("italic"); }); it("converts paragraph with link", () => { const content = `Visit our site
`; const result = gutenbergToPortableText(content); const block = result[0] as PortableTextTextBlock; expect(block.markDefs).toHaveLength(1); expect(block.markDefs?.[0]).toMatchObject({ _type: "link", href: "https://example.com", }); }); it("skips empty paragraphs", () => { const content = ` `; const result = gutenbergToPortableText(content); expect(result).toHaveLength(0); }); it("handles multiple paragraphs", () => { const content = `First paragraph
Second paragraph
`; const result = gutenbergToPortableText(content); expect(result).toHaveLength(2); }); }); describe("heading blocks", () => { it("converts h1", () => { const content = ``; const result = gutenbergToPortableText(content); expect(result[0]).toMatchObject({ _type: "block", style: "blockquote", }); }); it("handles quote with citation", () => { const content = `To be or not to be
`; const result = gutenbergToPortableText(content); // Should have quote block + citation block expect(result.length).toBeGreaterThanOrEqual(1); expect(result[0]).toMatchObject({ _type: "block", style: "blockquote", }); }); it("handles multi-paragraph quote", () => { const content = `To be or not to be
`; const result = gutenbergToPortableText(content); const quoteBlocks = result.filter((b) => (b as PortableTextTextBlock).style === "blockquote"); expect(quoteBlocks).toHaveLength(2); }); }); describe("image blocks", () => { it("converts image with URL in attrs", () => { const content = `First paragraph of quote
Second paragraph of quote





const x = 1;
`;
const result = gutenbergToPortableText(content);
expect(result[0]).toMatchObject({
_type: "code",
code: "const x = 1;",
});
});
it("preserves language attribute", () => {
const content = `
const x = 1;
`;
const result = gutenbergToPortableText(content);
expect(result[0]).toMatchObject({
_type: "code",
language: "javascript",
});
});
it("decodes HTML entities in code", () => {
const content = `
<div>Hello</div>
`;
const result = gutenbergToPortableText(content);
expect(result[0]).toMatchObject({
_type: "code",
code: "function hello() {
return "world";
}
`;
const result = gutenbergToPortableText(content);
expect((result[0] as { code: string }).code).toContain("\n");
});
});
describe("embed blocks", () => {
it("converts YouTube embed", () => {
const content = `
Column 1
Column 2
Paragraph in group
This is the introduction.

`; const result = gutenbergToPortableText(content); // h1 + p + image + 2 list items + quote = 6 blocks expect(result.length).toBeGreaterThanOrEqual(5); const types = result.map((b) => b._type); expect(types).toContain("block"); expect(types).toContain("image"); }); }); }); describe("htmlToPortableText", () => { it("converts simple HTML paragraphs", () => { const html = "A quote
Hello world
"; const result = htmlToPortableText(html); expect(result).toHaveLength(1); expect(result[0]).toMatchObject({ _type: "block", style: "normal", }); }); it("converts headings", () => { const html = "A quote"; const result = htmlToPortableText(html); expect(result[0]).toMatchObject({ _type: "block", style: "blockquote", }); }); it("converts code blocks", () => { const html = "
const x = 1;";
const result = htmlToPortableText(html);
expect(result[0]).toMatchObject({
_type: "code",
code: "const x = 1;",
});
});
it("converts horizontal rules", () => {
const html = "Before
After
"; const result = htmlToPortableText(html); const breakBlock = result.find((b) => b._type === "break"); expect(breakBlock).toBeDefined(); }); it("handles inline formatting", () => { const html = "Hello bold and italic
"; const result = htmlToPortableText(html); const block = result[0] as PortableTextTextBlock; expect(block.children.some((c) => c.marks?.includes("strong"))).toBe(true); expect(block.children.some((c) => c.marks?.includes("em"))).toBe(true); }); }); describe("WordPress.com classic editor content", () => { // Test case from sparge.wordpress.com - classic editor with linked images // and HTML entities in URLs (& instead of &) const spargePostContent = `
Hip Hops Nelson Sauvin is the first of my Christmas brews.
It's inspired by BrewDog 77, which is a classic lager dry-hopped with Nelson Sauvin. OK, so Hip Hops is 6.3% rather than 4.9%, and uses ordinary Saaz in the boil, but the essence is the same: it's a delicious, crisp German-style lager, given a New Zealand accent with a big hit of Nelson Sauvin.
`; it("extracts linked images with decoded URLs", () => { const result = htmlToPortableText(spargePostContent); // Should have at least one image block const imageBlocks = result.filter((b) => b._type === "image"); expect(imageBlocks.length).toBeGreaterThanOrEqual(1); // First block should be the image const img = imageBlocks[0]; expect(img._type).toBe("image"); // URL should have decoded HTML entities (& not &) expect(img.asset.url).toBe( "https://sparge.wordpress.com/wp-content/uploads/2011/11/hip-hop.jpg?w=205&h=300", ); expect(img.asset.url).not.toContain("&"); // Link should be preserved expect(img.link).toBe("https://sparge.wordpress.com/wp-content/uploads/2011/11/hip-hop.jpg"); }); it("preserves text content alongside images", () => { const result = htmlToPortableText(spargePostContent); // Should have text blocks with the paragraph content const textBlocks = result.filter((b) => b._type === "block"); expect(textBlocks.length).toBeGreaterThanOrEqual(1); // Check that text content is preserved const allText = textBlocks.flatMap((b) => b.children.map((c) => c.text)).join(""); expect(allText).toContain("Hip Hops Nelson Sauvin"); expect(allText).toContain("Christmas brews"); }); it("preserves links in text", () => { const result = htmlToPortableText(spargePostContent); // Should have text blocks with links const textBlocks = result.filter((b) => b._type === "block"); // Find block with BrewDog link const blockWithLink = textBlocks.find((b) => b.markDefs?.length); expect(blockWithLink).toBeDefined(); expect(blockWithLink?.markDefs).toContainEqual( expect.objectContaining({ _type: "link", href: "http://www.brewdog.com/product/77-lager", }), ); }); it("decodes HTML entities in standalone image src", () => { const html = `
`;
const result = htmlToPortableText(html);
expect(result).toHaveLength(1);
const img = result[0] as PortableTextImageBlock;
expect(img._type).toBe("image");
expect(img.asset.url).toBe("https://example.com/photo.jpg?w=200&h=300");
});
it("decodes & variant in URLs", () => {
const html = `


Hello
`; const blocks = parseGutenbergBlocks(content); expect(blocks).toHaveLength(1); expect(blocks[0]?.blockName).toBe("core/paragraph"); expect(blocks[0]?.innerHTML).toContain("Hello"); }); it("returns empty array for empty content", () => { expect(parseGutenbergBlocks("")).toEqual([]); }); it("preserves block attributes", () => { const content = `Nested