UNPKG

@automattic/rtjson-to-wpblocks

Version:

Javascript code to convert Day One RTJson to WordPress Gutenberg Blocks

226 lines (206 loc) 5.78 kB
// 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, };