@automattic/rtjson-to-wpblocks
Version:
Javascript code to convert Day One RTJson to WordPress Gutenberg Blocks
226 lines (206 loc) • 5.78 kB
JavaScript
// Docs for RTJson format:
// https://dayonep2.wordpress.com/documentation/tech-specs/richtextjson-format-specification/
// @ts-check
/// <reference path="./types.d.ts" />
const takeUntil = (arr, callback) => {
let index = 0;
while (index < arr.length && !callback(arr[index], index, arr)) {
index += 1;
}
return arr.slice(0, index + 1);
};
const takeWhile = (arr, callback) => {
let index = 0;
while (index < arr.length && callback(arr[index], index, arr)) {
index += 1;
}
return arr.slice(0, index);
};
const getNodeType = (node) => {
if (node.embeddedObjects) {
return "embedded";
} else {
return getTextNodeType(node);
}
};
function getTextNodeType(node) {
if (node.attributes?.line?.header) {
return "header";
} else if (node.attributes?.line?.listStyle) {
return node.attributes?.line?.listStyle;
} else if (node.attributes?.line?.codeBlock) {
return "code";
} else if (node.attributes?.line?.quote) {
return "quote";
} else {
return "paragraph";
}
}
function getEmptyParagraphNode() {
return { type: "paragraph", content: [] };
}
function getEmptyListItemNode() {
return { type: "listItem", content: [] };
}
function getQuoteNode(nodes) {
let quote = { type: "quote", content: [] };
const quoteNodes = takeWhile(nodes.slice(0), (n) => isQuoteNode(n));
let currentParagraph = getEmptyParagraphNode();
for (let j = 0; j < quoteNodes.length; j++) {
const node = quoteNodes[j];
currentParagraph.content.push({ type: "text", ...node });
if (isLineEnd(node)) {
quote.content.push(currentParagraph);
currentParagraph = getEmptyParagraphNode();
}
}
return { quote, nodeCount: quoteNodes.length };
}
function getListNode(nodes, type, level) {
const list = { type, content: [] };
let index = 0;
let currentListItem = getEmptyListItemNode();
let incompleteLine = true;
while (
index < nodes.length &&
nodes[index].attributes?.line?.listStyle &&
nodes[index].attributes?.line?.listStyle === type
) {
const node = nodes[index];
const indentLevel = getNodeIndentLevel(node);
if (indentLevel === level) {
// we save the previoys item if we are at the same level and the previous line ended
if (!incompleteLine) {
list.content.push(currentListItem);
currentListItem = getEmptyListItemNode();
incompleteLine = true;
}
currentListItem.content.push({ type: "text", ...node });
if (isLineEnd(node)) {
incompleteLine = false;
}
} else if (indentLevel > level) {
const listType = node.attributes?.line?.listStyle;
const { list: innerList, nodeCount } = getListNode(
nodes.slice(index),
listType,
indentLevel
);
currentListItem.content.push(innerList);
index += nodeCount;
continue;
}
index++;
}
list.content.push(currentListItem);
return { list, nodeCount: index };
}
function getTextNodes(nodes, nodeType) {
// Get all adjacent nodes of the same type
const siblingNodes = takeWhile(nodes, (n) => {
return getNodeType(n) === nodeType;
});
const textNodes = [];
let newNode;
for (let i = 0; i < siblingNodes.length; i++) {
const node = siblingNodes[i];
if (!newNode) {
newNode = {
type: nodeType,
content: [{ type: "text", ...node }],
};
} else {
newNode.content.push({ type: "text", ...node });
}
if (isLineEnd(node)) {
textNodes.push(newNode);
newNode = null;
}
}
if (newNode) {
textNodes.push(newNode);
}
return { textNodes: textNodes, nodeCount: siblingNodes.length };
}
const buildNodeTree = (RtjNodes) => {
const newNodes = [];
for (let i = 0; i < RtjNodes.length; ) {
const node = RtjNodes[i];
if (node.embeddedObjects) {
newNodes.push({ type: "embedded", content: node });
i++;
} else {
const nodeType = getTextNodeType(node);
if (
nodeType === "paragraph" ||
nodeType === "header" ||
nodeType === "code"
) {
const { textNodes, nodeCount } = getTextNodes(
RtjNodes.slice(i),
nodeType
);
newNodes.push(...textNodes);
i += nodeCount;
} else if (nodeType === "quote") {
const { quote, nodeCount } = getQuoteNode(RtjNodes.slice(i));
newNodes.push(quote);
i += nodeCount;
} else if (["bulleted", "numbered", "checklist"].includes(nodeType)) {
const indentLevel = getNodeIndentLevel(node);
const { list, nodeCount } = getListNode(
RtjNodes.slice(i),
nodeType,
indentLevel
);
newNodes.push(list);
i += nodeCount;
}
}
}
return newNodes;
};
/**
* @param {RTJNode} node
* @returns boolean
*/
function isBlockquoteNode(node) {
let lineAttributes = "attributes" in node ? node.attributes.line : null;
let hasEmbeddedObjects = "embeddedObjects" in node;
return !hasEmbeddedObjects && lineAttributes && lineAttributes.quote;
}
/**
* @param {RTJNode} node
* @returns boolean
*/
function isLineEnd(node) {
return "text" in node && node.text.endsWith("\n");
}
/**
* @param {RTJ.TextNode} node
* @returns boolean
*/
function shouldCloseTextBlock(node) {
return node.text && node.text.endsWith("\n");
}
/**
* @param {RTJ.TextNode} node
* @returns boolean
*/
function isQuoteNode(node) {
return "attributes" in node && node.attributes?.line?.quote;
}
/**
* @param {RTJNode} node
* @returns number
*/
function getNodeIndentLevel(node) {
if ("attributes" in node && node.attributes.line?.indentLevel !== null) {
return node.attributes.line?.indentLevel;
} else {
return 0;
}
}
module.exports = {
buildNodeTree,
};