UNPKG

@atlaskit/editor-core

Version:

A package contains Atlassian editor core functionality

197 lines (185 loc) • 7.19 kB
import { Fragment, Node as PMNode, NodeSpec, Schema } from '../../'; import { default as encodeCxhtml } from './encode-cxhtml'; import { children } from './utils'; export const docContentWrapper = (schema: Schema<any, any>, content: Fragment, convertedNodesReverted: WeakMap<Fragment | PMNode, Node>) => { const validContent = (node: PMNode) => { if (node.type.spec.group === 'block') { return true; } return false; }; // For doc we want to convert all UnsupportedInline to UnsupportedBlock const blockContent: PMNode[] = []; content.forEach((node: PMNode) => { if (node.type === schema.nodes.confluenceUnsupportedInline) { const unsupportedBlock: PMNode = schema.nodes.confluenceUnsupportedBlock.create({ cxhtml: node.attrs.cxhtml }); blockContent.push(unsupportedBlock); return; } blockContent.push(node); }); return ensureBlock(schema, Fragment.fromArray(blockContent), convertedNodesReverted, validContent); }; /** * @param content * @param convertedNodesReverted * Bullet List and Ordered List can only accept listItems */ export const listContentWrapper = (schema: Schema<any, any>, content: Fragment, convertedNodesReverted: WeakMap<Fragment | PMNode, Node>) => { const result: PMNode[] = []; content.forEach((node: PMNode) => { if (node.type !== schema.nodes.listItem) { const listItemContent = listItemContentWrapper(schema, Fragment.from(node), convertedNodesReverted); const listItem = schema.nodes.listItem.createChecked({}, listItemContent); result.push(listItem); } else { result.push(node); } }); return Fragment.fromArray(result); }; /** * @param node * @param convertedNodesReverted * A private method that used by listItemContentWrapper and blockquoteContentWrapper * to wrap invalid content in a paragraph */ const convertInvalidToParagraph = (schema: Schema<any, any>, node: PMNode, convertedNodesReverted: WeakMap<Fragment | PMNode, Node>) => { const paragraphContent = ensureInline(schema, Fragment.from(node), convertedNodesReverted); const paragraph = schema.nodes.paragraph.createChecked({}, paragraphContent); return paragraph; }; /** * @param content * @param convertedNodesReverted * ListItem content schema 'paragraph (paragraph | bulletList | orderedList)*' */ export const listItemContentWrapper = (schema: Schema<any, any>, content: Fragment, convertedNodesReverted: WeakMap<Fragment | PMNode, Node>) => { const validSpec: NodeSpec[] = [schema.nodes.paragraph, schema.nodes.bulletList, schema.nodes.orderedList]; const validContent = (node: PMNode) => { if (validSpec.some((spec: NodeSpec) => { return spec === node.type; })) { return true; } return false; }; const convertInvalid = (node: PMNode) => { return convertInvalidToParagraph(schema, node, convertedNodesReverted); }; return ensureBlock(schema, content, convertedNodesReverted, validContent, convertInvalid); }; /** * @param content * @param convertedNodesReverted * blockquote schema supports one or more number of paragraph nodes */ export const blockquoteContentWrapper = (schema: Schema<any, any>, content: Fragment, convertedNodesReverted: WeakMap<Fragment | PMNode, Node>) => { const validSpec: NodeSpec[] = [schema.nodes.paragraph]; const validContent = (node: PMNode) => { if (validSpec.some((spec: NodeSpec) => { return spec === node.type; })) { return true; } return false; }; const convertInvalid = (node: PMNode) => { return convertInvalidToParagraph(schema, node, convertedNodesReverted); }; return ensureBlock(schema, content, convertedNodesReverted, validContent, convertInvalid); }; /** * @param content * @param convertedNodesReverted * This function will convert all content to inline nodes */ export const ensureInline = (schema: Schema<any, any>, content: Fragment, convertedNodesReverted: WeakMap<Fragment | PMNode, Node>) => { const result: PMNode[] = []; content.forEach((node: PMNode) => { if (node.isInline) { result.push(node); return; } // We replace an non-inline node with UnsupportedInline node const originalNode = convertedNodesReverted.get(node) || convertedNodesReverted.get(content); const unsupportedInline: PMNode = schema.nodes.confluenceUnsupportedInline.create({ cxhtml: originalNode ? encodeCxhtml(originalNode) : '' }); result.push(unsupportedInline); }); return Fragment.fromArray(result); }; /** * Ensure that each node in the fragment is valid block, wrapping * in a block node if necessary. You pass in a * validContent to skip some of the content type * Optionaly, you can decide to how to convert invalid node */ export function ensureBlock(schema: Schema<any, any>, content: Fragment, convertedNodesReverted: WeakMap<Fragment | PMNode, Node>, validContent: (node: PMNode) => boolean, convertInvalid?: (node: PMNode) => PMNode): Fragment { // This algorithm is fairly simple: // // 1. If validContent(node) => true, keep it as-is. // 2. When a sequence of supported (i.e. *not* `unsupportedInline`) inlines is encountered, // wrap it in a paragraph. // 3. When an invalid block node is encountered, convert it with convertInvalid() // // It's seems possible for CXHTML documents to be poorly formed, where inline content exists // in positions where block content is expected. For example the top-level content is not wrapped // in a paragraph, but is expected to be a top-level block node. // // Foo bar baz // // In this scenario it's effectively wrapped in a paragraph: // // <p>Foo bar baz</p> // // This is more common in places like list items, or block quotes: // // <ul> // <li>Foo bar</li> // </ul> // <blockquote>Foo bar</blockquote> // // Both `<li>` (`listItem`) and `<blockquote>` (`blockQuote`) expect *paragraph* content, and so // in both cases `Foo bar` is wrapped in a paragraph. const nodes = children(content); const blocks: PMNode[] = []; const defaultConvertInvalid = (node: PMNode) => { const originalNode = convertedNodesReverted.get(node) || convertedNodesReverted.get(content); const unsupportedBlock: PMNode = schema.nodes.confluenceUnsupportedBlock.create({ cxhtml: originalNode ? encodeCxhtml(originalNode) : '' }); return unsupportedBlock; }; let i; for (i = 0; i < nodes.length; i++) { const node = nodes[i]; if (validContent(node)) { blocks.push(node); } else if (node.isInline) { // An inline node is found. Now step through until we find the last inline // node, then throw everything in a paragraph. let j = i + 1; for (j; j < nodes.length; j++) { const nextNode = nodes[j]; if (nextNode.isBlock) { break; } } blocks.push(schema.nodes.paragraph.createChecked({}, nodes.slice(i, j))); i = j-1; } else { // This is an block node but invalid content blocks.push(convertInvalid ? convertInvalid(node) : defaultConvertInvalid(node)); } } return Fragment.fromArray(blocks); }