@atlaskit/editor-wikimarkup-transformer
Version:
Wiki markup transformer for JIRA and Confluence
283 lines (268 loc) • 8.53 kB
JavaScript
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;
}
}