UNPKG

@atlaskit/editor-wikimarkup-transformer

Version:

Wiki markup transformer for JIRA and Confluence

283 lines (268 loc) 8.53 kB
import _defineProperty from "@babel/runtime/helpers/defineProperty"; const supportedContentType = ['paragraph', 'orderedList', 'bulletList', 'mediaSingle', 'codeBlock']; /** * Return the type of a list from the bullets */ export function getType(bullets) { // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp return /#$/.test(bullets) ? 'orderedList' : 'bulletList'; } export class ListBuilder { constructor(schema, bullets) { /** * Build prosemirror bulletList or orderedList node * @param {List} list * @returns {PMNode} */ _defineProperty(this, "parseList", list => { const listNode = this.schema.nodes[list.type]; const output = []; let listItemsBuffer = []; for (let i = 0; i < list.children.length; i++) { const parsedContent = this.parseListItem(list.children[i]); for (let j = 0; j < parsedContent.length; j++) { const parsedNode = parsedContent[j]; if (parsedNode.type.name === 'listItem') { listItemsBuffer.push(parsedNode); continue; } /** * If the node is not a listItem, then we need to * wrap exisintg list and break out */ if (listItemsBuffer.length) { const list = listNode.createChecked({}, listItemsBuffer); output.push(list); } output.push(parsedNode); // This is the break out node listItemsBuffer = []; } } if (listItemsBuffer.length) { const list = listNode.createChecked({}, listItemsBuffer); output.push(list); } return output; }); /** * Build prosemirror listItem node * This function would possibly return non listItem nodes * which we need to break out later * @param {ListItem} item */ _defineProperty(this, "parseListItem", item => { const output = []; if (!item.content) { item.content = []; } // Parse nested list const parsedChildren = item.children.reduce((result, list) => { const parsedList = this.parseList(list); result.push(...parsedList); return result; }, []); // Append children to the content item.content.push(...parsedChildren); let contentBuffer = []; for (let i = 0; i < item.content.length; i++) { const pmNode = item.content[i]; /** * Skip empty paragraph */ if (pmNode.type.name === 'paragraph' && pmNode.childCount === 0) { continue; } /* Skip Empty spaces after rule */ if (this.isParagraphEmptyTextNode(pmNode)) { continue; } if (supportedContentType.indexOf(pmNode.type.name) === -1) { const listItem = this.createListItem(contentBuffer, this.schema); output.push(listItem); output.push(pmNode); contentBuffer = []; continue; } contentBuffer.push(pmNode); } if (contentBuffer.length) { const listItem = this.createListItem(contentBuffer, this.schema); output.push(listItem); } return output; }); this.schema = schema; this.root = { children: [], type: getType(bullets) }; this.lastDepth = 1; this.lastList = this.root; } /** * Return the type of the base list * @returns {ListType} */ get type() { return this.root.type; } /** * Add a list item to the builder * @param {AddArgs[]} items */ add(items) { for (const item of items) { const { style, content } = item; // If there's no style, add to previous list item as multiline if (style === null) { this.appendToLastItem(content); continue; } const depth = style.length; const type = getType(style); if (depth > this.lastDepth) { // Add children starting from last node this.createNest(depth - this.lastDepth, type); this.lastDepth = depth; this.lastList = this.addListItem(type, content); } else if (depth === this.lastDepth) { // Add list item to current node this.lastList = this.addListItem(type, content); } else { // Find node at depth and add list item this.lastList = this.findAncestor(this.lastDepth - depth); this.lastDepth = depth; this.lastList = this.addListItem(type, content); } } } /** * Compile a prosemirror node from the root list * @returns {PMNode[]} */ buildPMNode() { return this.parseList(this.root); } /* Check if all paragraph's children nodes are text and empty */ isParagraphEmptyTextNode(node) { if (node.type.name !== 'paragraph' || !node.childCount) { return false; } for (let i = 0; i < node.childCount; i++) { const n = node.content.child(i); if (n.type.name !== 'text') { // Paragraph contains non-text node, so not empty return false; } else if (n.textContent.trim() !== '') { return false; } } return true; } createListItem(content, schema) { if (content.length === 0 || ['paragraph', 'mediaSingle'].indexOf(content[0].type.name) === -1) { // If the first element is a list node, try to create a wrapper listItem // (list as first child, no paragraph) for flexible list indentation. // If the schema doesn't support this variant, fall back to prepending // an empty paragraph. const listTypes = ['bulletList', 'orderedList', 'taskList']; if (content.length > 0 && listTypes.indexOf(content[0].type.name) !== -1) { try { return schema.nodes.listItem.createChecked({}, content); } catch { // Schema doesn't support list as first child of listItem, // fall back to prepending an empty paragraph } } // If the content is empty or the first element is not paragraph or mediaSingle, // this is likely a nested list where the top-level list item has no text content. // For example: *# item 1 // In this case we create an empty paragraph for the top level listNode. content.unshift(this.schema.nodes.paragraph.createChecked()); } return schema.nodes.listItem.createChecked({}, content); } /** * Add an item at the same level as the current list item * @param {ListType} type * @param {PMNode} content * @returns {PMNode} */ addListItem(type, content) { let list = this.lastList; // If the list is a different type, create a new list and add it to the parent node if (list.type !== type) { const parent = list.parent; const newList = { children: [], type, parent }; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion parent.children.push(newList); this.lastList = list = newList; } const listItem = { content, parent: list, children: [] }; list.children = [...list.children, listItem]; return list; } /** * Append the past content to the last accessed list node (multiline entries) * @param {PMNode[]} content */ appendToLastItem(content) { const { children } = this.lastList; const lastItem = children[children.length - 1]; // Ignored via go/ees005 // eslint-disable-next-line @typescript-eslint/no-non-null-assertion lastItem.content.push(...content); } /** * Created a nested list structure of N depth under the current node * @param {number} depth * @param {ListType} type */ createNest(depth, type) { while (depth-- > 0) { if (this.lastList.children.length === 0) { const listItem = { parent: this.lastList, children: [] }; this.lastList.children = [listItem]; } const nextItem = this.lastList.children[this.lastList.children.length - 1]; nextItem.children = [{ children: [], parent: nextItem, type }]; this.lastList = nextItem.children[0]; } } /** * Find the Nth list ancestor of the current list * @param {number} depth */ findAncestor(depth) { let list = this.lastList; while (depth-- > 0 && list.parent) { const listItem = list.parent; if (listItem && listItem.parent) { list = listItem.parent; } } return list; } }