@automattic/rtjson-to-wpblocks
Version:
Javascript code to convert Day One RTJson to WordPress Gutenberg Blocks
735 lines (669 loc) • 18.7 kB
JavaScript
// @ts-check
const dedent = require("dedent").default;
const { describe, it, expect } = require("@jest/globals");
const { rtjNodesToGBHtml } = require("../index.js");
const exp = require("constants");
const fs = require("fs").promises;
// 🚨 HEY! We're patching the String prototype here to make tests easier to write.
String.prototype.fixWhitespace = function () {
return this.replaceAll(/>[\n\r\s]+</g, ">\n<").replaceAll(/></g, ">\n<");
};
describe("Converting RTJson to GB Serialized Block format", () => {
it("Should convert a paragraph node", () => {
let node = {
attributes: {},
text: "It has a paragraph\n",
};
let expected = dedent`
<!-- wp:paragraph -->
<p>It has a paragraph</p>
<!-- /wp:paragraph -->
`;
expect(rtjNodesToGBHtml([node]).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should convert a header node", () => {
let node2 = {
attributes: {
line: {
header: 2,
},
},
text: "Header 2\n",
};
let expected2 = dedent`
<!-- wp:heading {"level":2} -->
<h2 class="wp-block-heading">Header 2</h2>
<!-- /wp:heading -->
`;
expect(rtjNodesToGBHtml([node2])).toBe(expected2);
let node3 = {
attributes: {
line: {
header: 3,
},
},
text: "Header 3\n",
};
// Quick strange note here. In actual testing with the GB editor, it seems that
// An h2 can have the "{level: 2}" part completely omitted, but the other levels
// require it. This library will always include it because that seems to be valid
// behavior according to GB experimentation.
let expected3 = dedent`
<!-- wp:heading {"level":3} -->
<h3 class="wp-block-heading">Header 3</h3>
<!-- /wp:heading -->
`;
expect(rtjNodesToGBHtml([node3])).toBe(expected3);
let nodeForAnH1 = {
attributes: {
line: {
header: 1,
},
},
text: "Header 1\n",
};
let expectedForAnH1 = dedent`
<!-- wp:heading {"level":1} -->
<h1 class="wp-block-heading">Header 1</h1>
<!-- /wp:heading -->
`;
expect(rtjNodesToGBHtml([nodeForAnH1])).toBe(expectedForAnH1);
});
it("should handle a simple list", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
attributes: {
line: {
listStyle: "bulleted",
indentLevel: 1,
},
},
text: "one\n",
},
{
attributes: {
line: {
listStyle: "bulleted",
indentLevel: 1,
},
},
text: "two\n",
},
];
expect(rtjNodesToGBHtml(nodes)).toMatchInlineSnapshot(`
"<!-- wp:list -->
<ul><!-- wp:list-item -->
<li>one</li>
<!-- /wp:list-item --><!-- wp:list-item -->
<li>two</li>
<!-- /wp:list-item --></ul>
<!-- /wp:list -->"
`);
});
it("Should handle complex lists", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
attributes: {
line: {
listStyle: "bulleted",
indentLevel: 1,
},
},
text: "And a list\n",
},
{
attributes: {
line: {
listStyle: "bulleted",
indentLevel: 2,
},
},
text: "Indented\n",
},
{
attributes: {
line: {
listStyle: "bulleted",
indentLevel: 3,
},
},
text: "again\n",
},
];
let expected = dedent`
<!-- wp:list -->
<ul><!-- wp:list-item -->
<li>And a list<!-- wp:list -->
<ul><!-- wp:list-item -->
<li>Indented<!-- wp:list -->
<ul><!-- wp:list-item -->
<li>again</li>
<!-- /wp:list-item --></ul>
<!-- /wp:list --></li>
<!-- /wp:list-item --></ul>
<!-- /wp:list --></li>
<!-- /wp:list-item --></ul>
<!-- /wp:list -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle different lists following each other", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
attributes: {
line: {
listStyle: "bulleted",
indentLevel: 1,
},
},
text: "And a bulleted list\n",
},
{
attributes: {
line: {
listStyle: "numbered",
indentLevel: 1,
},
},
text: "A numbered list\n",
},
{
attributes: {
line: {
listStyle: "numbered",
indentLevel: 1,
},
},
text: "numbered 2\n",
},
];
let expected = dedent`
<!-- wp:list -->
<ul><!-- wp:list-item -->
<li>And a bulleted list</li>
<!-- /wp:list-item --></ul>
<!-- /wp:list -->
<!-- wp:list -->
<ol><!-- wp:list-item -->
<li>A numbered list</li>
<!-- /wp:list-item -->
<!-- wp:list-item -->
<li>numbered 2</li>
<!-- /wp:list-item --></ol>
<!-- /wp:list -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle a photo node", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
embeddedObjects: [
{
type: "photo",
identifier: "B7C0EB0794D04FF3B496308ED46D179A",
},
],
},
];
/** @type {MediaLookup} */
let lookup = {
B7C0EB0794D04FF3B496308ED46D179A: {
id: 11,
url: "https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/IMG_7738-768x1024.jpeg",
},
};
let expected = dedent`
<!-- wp:image {"id":11,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/IMG_7738-768x1024.jpeg" alt="" class="wp-image-11"/></figure>
<!-- /wp:image -->
`;
expect(rtjNodesToGBHtml(nodes, lookup).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle a photo gallery node", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
embeddedObjects: [
{
type: "photo",
identifier: "B7C0EB0794D04FF3B496308ED46D179A",
},
{
type: "photo",
identifier: "A7C0EB0794D04DD3B496308ED46D179A",
},
],
},
];
/** @type {MediaLookup} */
let lookup = {
B7C0EB0794D04FF3B496308ED46D179A: {
id: 11,
url: "https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/IMG_7738-768x1024.jpeg",
},
A7C0EB0794D04DD3B496308ED46D179A: {
id: 12,
url: "https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/IMG_7738-768x1024.jpeg",
},
};
let expected = dedent`
<!-- wp:gallery -->
<figure class="wp-block-gallery has-nested-images">
<!-- wp:image {"id":11,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/IMG_7738-768x1024.jpeg" alt="" class="wp-image-11"/></figure>
<!-- /wp:image -->
<!-- wp:image {"id":12,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/IMG_7738-768x1024.jpeg" alt="" class="wp-image-12"/></figure>
<!-- /wp:image -->
</figure>
<!-- /wp:gallery -->
`;
expect(rtjNodesToGBHtml(nodes, lookup).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
// If the ID is a string in the output HTML, gutenberg will report the block as broken.
it("Should always write the photo ID as a number, even if the identifier is passed as a string", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
embeddedObjects: [
{
type: "photo",
identifier: "B7C0EB0794D04FF3B496308ED46D179A",
},
],
},
];
/** @type {MediaLookup} */
let lookup = {
B7C0EB0794D04FF3B496308ED46D179A: {
// @ts-expect-error: testing
id: "11",
url: "https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/IMG_7738-768x1024.jpeg",
},
};
let expected = dedent`
<!-- wp:image {"id":11,"sizeSlug":"large","linkDestination":"none"} -->
<figure class="wp-block-image size-large"><img src="https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/IMG_7738-768x1024.jpeg" alt="" class="wp-image-11"/></figure>
<!-- /wp:image -->
`;
expect(rtjNodesToGBHtml(nodes, lookup).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle a video node", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
embeddedObjects: [
{
type: "video",
identifier: "B7C0EB0794D04FF3B496308ED46D179A",
},
],
},
];
/** @type {MediaLookup} */
let lookup = {
B7C0EB0794D04FF3B496308ED46D179A: {
id: 18,
url: "https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/biking.mp4",
},
};
let expected = dedent`
<!-- wp:video {"id":18} -->
<figure class="wp-block-video">
<video controls src="https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/biking.mp4"></video>
</figure>
<!-- /wp:video -->
`;
expect(rtjNodesToGBHtml(nodes, lookup).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle an audio node", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
embeddedObjects: [
{
type: "audio",
identifier: "B7C0EB0794D04FF3B496308ED46D179A",
},
],
},
];
/** @type {MediaLookup} */
let lookup = {
B7C0EB0794D04FF3B496308ED46D179A: {
id: 21,
url: "https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/ForTheDirtMan.mp3",
},
};
let expected = dedent`
<!-- wp:audio {"id":21} -->
<figure class="wp-block-audio"><audio controls src="https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/ForTheDirtMan.mp3"></audio></figure>
<!-- /wp:audio -->
`;
expect(rtjNodesToGBHtml(nodes, lookup).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle a PDF node", () => {
/** @type {Array<RTJ.Node>} */
let nodes = [
{
embeddedObjects: [
{
type: "pdfAttachment",
identifier: "B7C0EB0794D04FF3B496308ED46D179A",
},
],
},
];
/** @type {MediaLookup} */
let lookup = {
B7C0EB0794D04FF3B496308ED46D179A: {
id: 17,
url: "https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/it-is-a-mystery.pdf",
},
};
let expected = dedent`
<!-- wp:file {"id":17,"href":"https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/it-is-a-mystery.pdf","displayPreview":true} -->
<div class="wp-block-file"><object class="wp-block-file__embed" data="https://testposting.mystagingwebsite.com/wp-content/uploads/2024/06/it-is-a-mystery.pdf" type="application/pdf" style="width:100%;height:600px"></object></div>
<!-- /wp:file -->
`;
expect(rtjNodesToGBHtml(nodes, lookup).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle italics and bold in the middle of a paragraph", () => {
/** @type {Array<RTJNode>} */
let nodes = [
{
attributes: {},
text: "Here is a paragraph ",
},
{
attributes: {
bold: true,
},
text: "with bold text",
},
{
attributes: {},
text: " and ",
},
{
attributes: {
italic: true,
},
text: "with italicized text",
},
{
attributes: {},
text: " and ",
},
{
attributes: {
bold: true,
italic: true,
},
text: "with both",
},
{
attributes: {},
text: ".\n",
},
];
let expected = dedent`
<!-- wp:paragraph -->
<p>Here is a paragraph <strong>with bold text</strong> and <em>with italicized text</em> and <strong><em>with both</em></strong>.</p>
<!-- /wp:paragraph -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle highlighted text in the middle of a paragraph", () => {
let color = "#ffdddd";
/** @type {Array<RTJNode>} */
let nodes = [
{
attributes: {},
text: "Here is a paragraph ",
},
{
attributes: {
highlightedColor: color,
},
text: "with highlighted text",
},
{
attributes: {},
text: ".\n",
},
];
let expected = dedent`
<!-- wp:paragraph -->
<p>Here is a paragraph <mark data-rich-text-format-boundary="true" style="background-color:${color}" class="has-inline-color">with highlighted text</mark>.</p>
<!-- /wp:paragraph -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("Should handle inlines code in the middle of a paragraph", () => {
/** @type {Array<RTJNode>} */
let nodes = [
{
attributes: {},
text: "Here is a paragraph ",
},
{
attributes: {
inlineCode: true,
},
text: "with some code",
},
{
attributes: {},
text: " in it.\n",
},
];
let expected = dedent`
<!-- wp:paragraph -->
<p>Here is a paragraph <code>with some code</code> in it.</p>
<!-- /wp:paragraph -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("should handle block quote", () => {
/** @type {Array<RTJNode>} */
let nodes = [
{
attributes: {
line: {
quote: true,
},
},
text: "Here is a quote block\n",
},
];
let expected = dedent`
<!-- wp:quote -->
<blockquote class="wp-block-quote"><!-- wp:paragraph -->
<p>Here is a quote block</p>
<!-- /wp:paragraph --></blockquote>
<!-- /wp:quote -->`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("should handle a multiple line block quote", () => {
/** @type {Array<RTJNode>} */
let nodes = [
{
attributes: {
line: {
quote: true,
},
},
text: "Here is a quote block\n",
},
{
attributes: {
line: {
quote: true,
},
},
text: "Here's the second line\n",
},
];
let expected = dedent`
<!-- wp:quote -->
<blockquote class="wp-block-quote">
<!-- wp:paragraph -->
<p>Here is a quote block</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Here's the second line</p>
<!-- /wp:paragraph -->
</blockquote>
<!-- /wp:quote -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("should handle a code block", () => {
/** @type {Array<RTJNode>} */
let nodes = [
{
attributes: {
line: {
codeBlock: true,
},
},
text: "Here is a code block\n",
},
];
let expected = dedent`
<!-- wp:code -->
<pre class="wp-block-code"><code>Here is a code block</code>
</pre>
<!-- /wp:code -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("should handle a horizontal ruler", () => {
/** @type {Array<RTJNode>} */
let nodes = [
{
embeddedObjects: [{ type: "horizontalRuleLine" }],
},
];
let expected = dedent`
<!-- wp:separator -->
<hr class="wp-block-separator has-alpha-channel-opacity"/>
<!-- /wp:separator -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("should handle multiple quote nodes correctly", () => {
/** @type {Array<RTJNode>} */
let nodes = [
{
text: "First quote line ",
attributes: {
line: { quote: true },
},
},
{
text: "still first paragraph\n",
attributes: {
line: { quote: true },
},
},
{
text: "Second paragraph in quote\n",
attributes: {
line: { quote: true },
},
},
// Add a non-quote node to ensure quote block closes properly
{
text: "Regular paragraph\n",
},
];
let expected = dedent`
<!-- wp:quote -->
<blockquote class="wp-block-quote">
<!-- wp:paragraph -->
<p>First quote line still first paragraph</p>
<!-- /wp:paragraph -->
<!-- wp:paragraph -->
<p>Second paragraph in quote</p>
<!-- /wp:paragraph -->
</blockquote>
<!-- /wp:quote -->
<!-- wp:paragraph -->
<p>Regular paragraph</p>
<!-- /wp:paragraph -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
it("should handle quotes with text formatting", () => {
/** @type {Array<RTJNode>} */
let nodes = [
{
text: "Quote with ",
attributes: {
line: { quote: true },
bold: true,
},
},
{
text: "mixed formatting\n",
attributes: {
line: { quote: true },
italic: true,
},
},
];
let expected = dedent`
<!-- wp:quote -->
<blockquote class="wp-block-quote">
<!-- wp:paragraph -->
<p><strong>Quote with </strong><em>mixed formatting</em>
</p>
<!-- /wp:paragraph -->
</blockquote>
<!-- /wp:quote -->
`;
expect(rtjNodesToGBHtml(nodes).fixWhitespace()).toBe(
expected.fixWhitespace()
);
});
});