@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
358 lines (325 loc) • 11.2 kB
text/typescript
import * as namedColors from 'css-color-names';
import {
isSchemaWithLists,
isSchemaWithMentions,
isSchemaWithLinks,
isSchemaWithAdvancedTextFormattingMarks,
isSchemaWithCodeBlock,
isSchemaWithBlockQuotes,
isSchemaWithMedia,
isSchemaWithSubSupMark,
isSchemaWithTextColor,
isSchemaWithTables
} from './schema';
import {
Fragment,
Mark,
Node as PMNode,
Schema,
} from '../../prosemirror';
/**
* Ensure that each node in the fragment is a block, wrapping
* in a block node if necessary.
*/
export function ensureBlocks(fragment: Fragment, schema: Schema<any, any>): Fragment {
// If all the nodes are inline, we want to wrap in a single paragraph.
if (schema.nodes.paragraph.validContent(fragment)) {
return Fragment.fromArray([schema.nodes.paragraph.createChecked({}, fragment)]);
}
// Either all the nodes are blocks, or a mix of inline and blocks.
// We convert each (if any) inline nodes to blocks.
const blockNodes: PMNode[] = [];
fragment.forEach(child => {
if (child.isBlock) {
blockNodes.push(child);
} else {
blockNodes.push(schema.nodes.paragraph.createChecked({}, child));
}
});
return Fragment.fromArray(blockNodes);
}
export function convert(content: Fragment, node: Node, schema: Schema<any, any>): Fragment | PMNode | null | undefined {
// text
if (node.nodeType === Node.TEXT_NODE) {
const text = node.textContent;
return text ? schema.text(text) : null;
}
// marks and nodes
if (node instanceof HTMLElement) {
const tag = node.tagName.toUpperCase();
switch (tag) {
// Marks
case 'DEL':
if (!isSchemaWithAdvancedTextFormattingMarks(schema)) {
return null;
}
return addMarks(content, [schema.marks.strike!.create()]);
case 'B':
return addMarks(content, [schema.marks.strong.create()]);
case 'EM':
return addMarks(content, [schema.marks.em.create()]);
case 'TT':
if (!isSchemaWithAdvancedTextFormattingMarks(schema)) {
return null;
}
return addMarks(content, [schema.marks.code!.create()]);
case 'SUB':
case 'SUP':
if (!isSchemaWithSubSupMark(schema)) {
return null;
}
const type = tag === 'SUB' ? 'sub' : 'sup';
return addMarks(content, [schema.marks.subsup.create({ type })]);
case 'INS':
return addMarks(content, [schema.marks.underline.create()]);
case 'FONT':
if (!isSchemaWithTextColor(schema)) {
return null;
}
const color = getValidColor(node.getAttribute('color'));
return color ? addMarks(content, [schema.marks.textColor.create({ color })]) : content;
// Nodes
case 'A':
if (node.className === 'user-hover' && isSchemaWithMentions(schema)) {
return schema.nodes.mention!.createChecked({
id: node.getAttribute('rel'),
text: node.innerText
});
}
const isAnchor = node.attributes.getNamedItem('href') === null;
if (isAnchor || node.className.match('jira-issue-macro-key') || !content || !isSchemaWithLinks(schema)) {
return null;
}
return addMarks(
content,
[schema.marks.link!.create({
href: node.getAttribute('href'),
title: node.getAttribute('title')
})]
);
case 'SPAN':
/**
* JIRA ISSUE MACROS
* `````````````````
* <span class="jira-issue-macro" data-jira-key="ED-1">
* <a href="https://product-fabric.atlassian.net/browse/ED-1" class="jira-issue-macro-key issue-link">
* <img class="icon" src="./epic.svg" />
* ED-1
* </a>
* <span class="aui-lozenge aui-lozenge-subtle aui-lozenge-current jira-macro-single-issue-export-pdf">
* In Progress
* </span>
* </span>
*/
if (node.className === 'jira-issue-macro') {
const jiraKey = node.dataset.jiraKey;
return jiraKey ? schema.text(jiraKey) : null;
} else if (node.className.match('jira-macro-single-issue-export-pdf')) {
return null;
} else if (node.className.match('code-')) { // Removing spans with syntax highlighting from JIRA
return null;
} else if (isMedia(node) && isSchemaWithMedia(schema)) {
const dataNode = node.querySelector('[data-media-services-id]');
if (dataNode && dataNode instanceof HTMLElement) {
const {
mediaServicesId: id,
mediaServicesType: type,
mediaServicesCollection: collection = '',
attachmentName, attachmentType,
fileName, displayType
} = dataNode.dataset;
return schema.nodes.media.create({
id, type, collection,
__fileName: attachmentName || fileName,
__displayType: attachmentType || displayType || 'thumbnail',
});
}
}
break;
case 'IMG':
if (node.parentElement && node.parentElement.className.match('jira-issue-macro-key')) {
return null;
}
break;
case 'H1':
case 'H2':
case 'H3':
case 'H4':
case 'H5':
case 'H6':
const level = Number(tag.charAt(1));
return schema.nodes.heading.createChecked({ level }, content);
case 'BR':
return schema.nodes.hardBreak.createChecked();
case 'HR':
return schema.nodes.rule.createChecked();
case 'P':
if (node.firstChild && (isMedia(node.firstChild))) {
// Filter out whitespace text nodes
const mediaContent: Array<PMNode> = [];
let hasNonMediaChildren = false;
content.forEach(child => {
if (child.type === schema.nodes.media) {
mediaContent.push(child);
}
else if (!(child.isText && /^\s*$/.test(child.text || ''))){
hasNonMediaChildren = true;
}
});
if (hasNonMediaChildren) {
return schema.nodes.paragraph.createChecked({}, content);
}
if (isSchemaWithMedia(schema)) {
return schema.nodes.mediaGroup.createChecked({}, Fragment.fromArray(mediaContent));
}
return null;
}
return schema.nodes.paragraph.createChecked({}, content);
}
// lists
if (isSchemaWithLists(schema)) {
switch (tag) {
case 'UL':
return schema.nodes.bulletList!.createChecked({}, content);
case 'OL':
return schema.nodes.orderedList!.createChecked({}, content);
case 'LI':
const compatibleContent = schema.nodes.listItem!.validContent(content)
? content
: ensureBlocks(content, schema);
return schema.nodes.listItem!.createChecked({}, compatibleContent);
}
}
// code block
if (isSchemaWithCodeBlock(schema)) {
switch (tag) {
case 'DIV':
if (node.className === 'codeContent panelContent' || node.className.match('preformattedContent')) {
return null;
} else if (node.className === 'code panel' || node.className === 'preformatted panel') {
const pre = node.querySelector('pre');
if (!pre) {
return null;
}
const language = node.className === 'preformatted panel'
? 'plain'
: pre.className.split('-')[1];
const textContent = pre.innerText.replace(/\r\n/g, '\n');
return schema.nodes.codeBlock!.createChecked(
{ language },
textContent
? schema.text(textContent)
: undefined
);
}
break;
case 'PRE':
return null;
}
}
if (isSchemaWithBlockQuotes(schema) && tag === 'BLOCKQUOTE') {
let blockquoteContent = content && (content as any).content.length ? content : schema.nodes.paragraph.create();
return schema.nodes.blockquote!.createChecked({}, blockquoteContent);
}
// table
if (isSchemaWithTables(schema)) {
switch (tag) {
case 'TABLE':
return schema.nodes.table.createChecked({}, content);
case 'TR':
return schema.nodes.tableRow!.createChecked({}, content);
case 'TD':
const tdContent = schema.nodes.tableCell.validContent(content)
? content
: ensureBlocks(content, schema);
return schema.nodes.tableCell.createChecked({}, tdContent);
case 'TH':
const thContent = schema.nodes.tableHeader.validContent(content)
? content
: ensureBlocks(content, schema);
return schema.nodes.tableHeader.createChecked({}, thContent);
}
}
}
}
/*
* Flattens DOM tree into single array
*/
export function bfsOrder(root: Node): Node[] {
const inqueue = [root];
const outqueue = [] as Node[];
let elem;
while (elem = inqueue.shift()) {
outqueue.push(elem);
let childIndex;
for (childIndex = 0; childIndex < elem.childNodes.length; childIndex++) {
const child = elem.childNodes[childIndex];
switch (child.nodeType) {
case Node.ELEMENT_NODE:
case Node.TEXT_NODE:
inqueue.push(child);
break;
default:
console.error(`Not pushing: ${child.nodeType} ${child.nodeName}`);
}
}
}
outqueue.shift();
return outqueue;
}
/**
* Create a fragment by adding a set of marks to each node.
*/
function addMarks(fragment: Fragment, marks: Mark[]): Fragment {
let result = fragment;
for (let i = 0; i < fragment.childCount; i++) {
const child = result.child(i);
let newChild = child;
for (const mark of marks) {
newChild = newChild.mark(mark.addToSet(newChild.marks));
}
result = result.replaceChild(i, newChild);
}
return result;
}
function getValidColor(color: string | null): string | null {
if (!color) {
return null;
}
// Normalize
color = color.trim().toLowerCase();
if (color[0] === '#' && color.length === 4 || color.length === 7) {
if (/^#[\da-f]{3}$/.test(color)) {
color = color.split('').map(c => c === '#' ? '#' : `${c}${c}`).join('');
}
} else {
// http://dev.w3.org/csswg/css-color/#named-colors
if (namedColors[color]) {
color = namedColors[color];
}
else {
return null;
}
}
// Default colour from old JIRA colour palette
if (color === '#333333') {
return null;
}
return color;
}
function getNodeName(node: Node): string {
return node.nodeName.toUpperCase();
}
function isMedia(node: Node): boolean {
if (node && node instanceof HTMLElement) {
if (node.parentNode && getNodeName(node.parentNode) === 'P') {
if (getNodeName(node) === 'SPAN') {
return !!node.querySelector(
'a > jira-attachment-thumbnail > img[data-attachment-type="thumbnail"], ' +
'a[data-attachment-type="file"]'
);
}
}
}
return false;
}