@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
197 lines (185 loc) • 7.19 kB
text/typescript
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);
}