@atlaskit/editor-common
Version:
A package that contains common classes and components for editor and renderer
996 lines (982 loc) • 28.2 kB
JavaScript
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import _toConsumableArray from "@babel/runtime/helpers/toConsumableArray";
function ownKeys(e, r) { var t = Object.keys(e); if (Object.getOwnPropertySymbols) { var o = Object.getOwnPropertySymbols(e); r && (o = o.filter(function (r) { return Object.getOwnPropertyDescriptor(e, r).enumerable; })), t.push.apply(t, o); } return t; }
function _objectSpread(e) { for (var r = 1; r < arguments.length; r++) { var t = null != arguments[r] ? arguments[r] : {}; r % 2 ? ownKeys(Object(t), !0).forEach(function (r) { _defineProperty(e, r, t[r]); }) : Object.getOwnPropertyDescriptors ? Object.defineProperties(e, Object.getOwnPropertyDescriptors(t)) : ownKeys(Object(t)).forEach(function (r) { Object.defineProperty(e, r, Object.getOwnPropertyDescriptor(t, r)); }); } return e; }
import { inlineNodes, isSafeUrl, PanelType, generateUuid as uuid } from '@atlaskit/adf-schema';
import { defaultSchema } from '@atlaskit/adf-schema/schema-default';
import { fg } from '@atlaskit/platform-feature-flags';
export var ADFStages = {
FINAL: 'final',
STAGE_0: 'stage0'
};
/*
* An ADF Document JSON object. The document is the root node and documents are
* composed of nodes. This type accepts an array of ADNode types as content.
*
* It is basically the same as the JSONNodeDoc interface from editor-json-transformer.
*
* Do not use this type for content nodes as they require additional attributes.
*
* Use ADNode instead for content nodes (any node other than the doc).
*/
/*
* An ADF Node object. This type is used as content for the ADDoc interface.
* It is basically the same as the JSONNode type from editor-json-transformer
* but the types are a little more strict.
*
* It is a serialisable form of ADFEntity.
*
* Do not use this for ADF documents - they should use the ADDoc interface.
*/
/*
* It's important that this order follows the marks rank defined here:
* https://product-fabric.atlassian.net/wiki/spaces/ETEMP/pages/11174043/Atlassian+Document+Format+-+Internal+documentation#Rank
*/
export var markOrder = ['fragment', 'link', 'em', 'strong', 'textColor', 'backgroundColor', 'strike', 'subsup', 'underline', 'code', 'confluenceInlineComment', 'annotation', 'dataConsumer'];
export var isSubSupType = function isSubSupType(type) {
return type === 'sub' || type === 'sup';
};
/*
* Sorts mark by the predefined order above
*/
export var getMarksByOrder = function getMarksByOrder(marks) {
return _toConsumableArray(marks).sort(function (a, b) {
return markOrder.indexOf(a.type.name) - markOrder.indexOf(b.type.name);
});
};
/*
* Check if two marks are the same by comparing type and attrs
*/
export var isSameMark = function isSameMark(mark, otherMark) {
if (!mark || !otherMark) {
return false;
}
return mark.eq(otherMark);
};
export var getValidDocument = function getValidDocument(doc) {
var schema = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultSchema;
var adfStage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'final';
var node = getValidNode(doc, schema, adfStage);
if (node.type === 'doc') {
node.content = wrapInlineNodes(node.content);
return node;
}
return null;
};
var wrapInlineNodes = function wrapInlineNodes() {
var nodes = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : [];
return nodes.map(function (node) {
return inlineNodes.has(node.type) ? {
type: 'paragraph',
content: [node]
} : node;
});
};
export var getValidContent = function getValidContent(content) {
var schema = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultSchema;
var adfStage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'final';
return content.map(function (node) {
return getValidNode(node, schema, adfStage);
});
};
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var TEXT_COLOR_PATTERN = /^#[0-9a-fA-F]{6}$/;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var RELATIVE_LINK = /^\//;
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
var ANCHOR_LINK = /^#/;
var flattenUnknownBlockTree = function flattenUnknownBlockTree(node) {
var schema = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultSchema;
var adfStage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'final';
var output = [];
var isPrevLeafNode = false;
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
for (var i = 0; i < node.content.length; i++) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-non-null-assertion
var childNode = node.content[i];
var isLeafNode = !(childNode.content && childNode.content.length);
if (i > 0) {
if (isPrevLeafNode) {
output.push({
type: 'text',
text: ' '
});
} else {
output.push({
type: 'hardBreak'
});
}
}
if (isLeafNode) {
output.push(getValidNode(childNode, schema, adfStage));
} else {
output.push.apply(output, _toConsumableArray(flattenUnknownBlockTree(childNode, schema, adfStage)));
}
isPrevLeafNode = isLeafNode;
}
return output;
};
/**
* Sanitize unknown node tree
*
* @see https://product-fabric.atlassian.net/wiki/spaces/E/pages/11174043/Document+structure#Documentstructure-ImplementationdetailsforHCNGwebrenderer
*/
export var getValidUnknownNode = function getValidUnknownNode(node) {
var _node$attrs = node.attrs,
attrs = _node$attrs === void 0 ? {} : _node$attrs,
content = node.content,
text = node.text,
type = node.type;
if (!content || !content.length) {
var unknownInlineNode = {
type: 'text',
text: text || attrs.text || "[".concat(type, "]")
};
var textUrl = attrs.textUrl;
if (textUrl && isSafeUrl(textUrl)) {
unknownInlineNode.marks = [{
type: 'link',
attrs: {
href: textUrl
}
}];
}
return unknownInlineNode;
}
/*
* Find leaf nodes and join them. If leaf nodes' parent node is the same node
* join with a blank space, otherwise they are children of different branches, i.e.
* we need to join them with a hardBreak node
*/
return {
type: 'unknownBlock',
content: flattenUnknownBlockTree(node)
};
};
var getValidMarks = function getValidMarks(marks) {
var adfStage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'final';
if (marks && marks.length > 0) {
return marks.reduce(function (acc, mark) {
var validMark = getValidMark(mark, adfStage);
if (validMark) {
acc.push(validMark);
}
return acc;
}, []);
}
return marks;
};
/*
* This method will validate a Node according to the spec defined here
* https://product-fabric.atlassian.net/wiki/spaces/E/pages/11174043/Document+structure#Documentstructure-Nodes
*
* This is also the place to handle backwards compatibility.
*
* If a node is not recognized or is missing required attributes, we should return 'unknown'
*
*/
export var getValidNode = function getValidNode(originalNode) {
var schema = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : defaultSchema;
var adfStage = arguments.length > 2 && arguments[2] !== undefined ? arguments[2] : 'final';
var attrs = originalNode.attrs,
marks = originalNode.marks,
text = originalNode.text,
type = originalNode.type;
var content = originalNode.content;
var node = {
attrs: attrs,
marks: marks,
text: text,
type: type
};
if (content) {
node.content = content = getValidContent(content, schema, adfStage);
}
// If node type doesn't exist in schema, make it an unknown node
if (!schema.nodes[type]) {
return getValidUnknownNode(node);
}
if (type) {
switch (type) {
case 'doc':
{
var _ref = originalNode,
version = _ref.version;
if (version && content && content.length) {
return {
type: type,
content: content
};
}
break;
}
case 'codeBlock':
{
if (content) {
content = content.reduce(function (acc, val) {
if (val.type === 'text') {
acc.push({
type: val.type,
text: val.text
});
}
return acc;
}, []);
}
if (attrs && attrs.language) {
return {
type: type,
attrs: attrs,
content: content,
marks: marks
};
}
return {
type: type,
content: content,
marks: marks
};
}
case 'date':
{
if (attrs && attrs.timestamp) {
return _objectSpread({
type: type,
attrs: attrs
}, fg('editor_inline_comments_on_inline_nodes') ? {
marks: marks
} : {});
}
break;
}
case 'status':
{
if (attrs && attrs.text && attrs.color) {
return _objectSpread({
type: type,
attrs: attrs
}, fg('editor_inline_comments_on_inline_nodes') ? {
marks: marks
} : {});
}
break;
}
case 'emoji':
{
if (attrs && attrs.shortName) {
return _objectSpread({
type: type,
attrs: attrs
}, fg('editor_inline_comments_on_inline_nodes') ? {
marks: marks
} : {});
}
break;
}
case 'inlineExtension':
case 'extension':
{
if (attrs && attrs.extensionType && attrs.extensionKey) {
return {
type: type,
attrs: attrs
};
}
break;
}
case 'inlineCard':
{
if (attrs && (attrs.datasource && !attrs.url || attrs.url && isSafeUrl(attrs.url) || attrs.data && attrs.data.url && isSafeUrl(attrs.data.url))) {
return _objectSpread({
type: type,
attrs: attrs
}, fg('editor_inline_comments_on_inline_nodes') ? {
marks: marks
} : {});
}
break;
}
case 'blockCard':
{
if (attrs && (attrs.datasource && !attrs.url || attrs.url && isSafeUrl(attrs.url) || attrs.data && attrs.data.url && isSafeUrl(attrs.data.url))) {
return {
type: type,
attrs: attrs
};
}
break;
}
case 'embedCard':
{
if (attrs && (attrs.url && isSafeUrl(attrs.url) || attrs.data && attrs.data.url && isSafeUrl(attrs.data.url)) && attrs.layout) {
return {
type: type,
attrs: attrs
};
}
break;
}
case 'bodiedExtension':
{
if (attrs && attrs.extensionType && attrs.extensionKey && content) {
return {
type: type,
attrs: attrs,
content: content
};
}
break;
}
case 'multiBodiedExtension':
{
if (attrs && attrs.extensionType && attrs.extensionKey && content) {
return {
type: type,
attrs: attrs,
content: content
};
}
break;
}
case 'extensionFrame':
{
if (content) {
return {
type: type,
attrs: attrs,
content: content
};
}
break;
}
case 'hardBreak':
{
return {
type: type
};
}
case 'caption':
{
if (content) {
return {
type: type,
content: content
};
}
break;
}
case 'mediaInline':
{
var mediaId = '';
var mediaCollection = [];
if (attrs) {
var id = attrs.id,
collection = attrs.collection;
mediaId = id;
mediaCollection = collection;
}
if (mediaId && mediaCollection) {
return {
type: type,
attrs: attrs,
marks: marks
};
}
break;
}
case 'media':
{
var _mediaId = '';
var mediaType = '';
var _mediaCollection = [];
var mediaUrl = '';
if (attrs) {
var _id = attrs.id,
_collection = attrs.collection,
_type = attrs.type,
url = attrs.url;
_mediaId = _id;
mediaType = _type;
_mediaCollection = _collection;
mediaUrl = url;
}
if (mediaType === 'external' && !!mediaUrl) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var mediaAttrs = {
type: mediaType,
url: mediaUrl,
width: attrs.width,
height: attrs.height
};
if (attrs.alt) {
mediaAttrs.alt = attrs.alt;
}
var getMarks = getValidMarks(marks, adfStage);
return getMarks ? {
type: type,
attrs: mediaAttrs,
marks: getMarks
} : {
type: type,
attrs: mediaAttrs
};
} else if (_mediaId && mediaType) {
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var _mediaAttrs = {
type: mediaType,
id: _mediaId,
collection: _mediaCollection
};
if (attrs.width) {
_mediaAttrs.width = attrs.width;
}
if (attrs.height) {
_mediaAttrs.height = attrs.height;
}
if (attrs.alt) {
_mediaAttrs.alt = attrs.alt;
}
var _getMarks = getValidMarks(marks, adfStage);
return _getMarks ? {
type: type,
attrs: _mediaAttrs,
marks: _getMarks
} : {
type: type,
attrs: _mediaAttrs
};
}
break;
}
case 'mediaGroup':
{
if (Array.isArray(content) && !content.some(function (e) {
return e.type !== 'media';
})) {
return {
type: type,
content: content
};
}
break;
}
case 'mediaSingle':
{
var containsJustMedia = Array.isArray(content) && content.length === 1 && content[0].type === 'media';
var containsMediaAndCaption = Array.isArray(content) && content.length === 2 && content[0].type === 'media' && content[1].type === 'caption';
if (containsJustMedia || containsMediaAndCaption) {
return {
type: type,
attrs: attrs,
content: content,
marks: getValidMarks(marks, adfStage)
};
}
break;
}
case 'mention':
{
var mentionText = '';
var mentionId;
var mentionAccess;
if (attrs) {
var _text = attrs.text,
displayName = attrs.displayName,
_id2 = attrs.id,
accessLevel = attrs.accessLevel;
mentionText = _text || displayName;
mentionId = _id2;
mentionAccess = accessLevel;
}
if (!mentionText) {
mentionText = text || '@unknown';
}
if (mentionText && mentionId) {
var mentionNode = _objectSpread({
type: type,
attrs: {
id: mentionId,
text: mentionText,
accessLevel: ''
}
}, fg('editor_inline_comments_on_inline_nodes') ? {
marks: marks
} : {});
if (mentionAccess) {
mentionNode.attrs.accessLevel = mentionAccess;
}
return mentionNode;
}
break;
}
case 'paragraph':
{
if (adfStage === 'stage0') {
var paragraphNode = {
type: type,
content: content || []
};
if (attrs && attrs.localId) {
paragraphNode.attrs = {
localId: attrs.localId
};
}
if (marks) {
paragraphNode.marks = _toConsumableArray(marks);
}
return paragraphNode;
}
return marks ? {
type: type,
content: content || [],
marks: marks
} : {
type: type,
content: content || []
};
}
case 'rule':
{
return {
type: type
};
}
case 'text':
{
var _marks = node.marks;
if (text) {
return _marks ? {
type: type,
text: text,
marks: getValidMarks(_marks, adfStage)
} : {
type: type,
text: text
};
}
break;
}
case 'heading':
{
if (attrs) {
var level = attrs.level;
var between = function between(x, a, b) {
return x >= a && x <= b;
};
if (level && between(level, 1, 6)) {
if (adfStage === 'stage0') {
var headingNode = {
type: type,
content: content,
attrs: {
level: level
}
};
if (attrs.localId) {
headingNode.attrs.localId = attrs.localId;
}
if (marks) {
headingNode.marks = _toConsumableArray(marks);
}
return headingNode;
}
return marks ? {
type: type,
content: content,
marks: marks,
attrs: {
level: level
}
} : {
type: type,
content: content,
attrs: {
level: level
}
};
}
}
break;
}
case 'bulletList':
{
if (content) {
return {
type: type,
content: content
};
}
break;
}
case 'orderedList':
{
if (content) {
return {
type: type,
content: content,
attrs: {
order: attrs && attrs.order
}
};
}
break;
}
case 'listItem':
{
if (content) {
return {
type: type,
content: wrapInlineNodes(content)
};
}
break;
}
case 'blockquote':
{
if (content) {
return {
type: type,
content: content
};
}
break;
}
case 'panel':
{
if (attrs && content) {
var panelType = attrs.panelType;
if (Object.values(PanelType).includes(panelType)) {
return {
type: type,
attrs: attrs,
content: content
};
}
}
break;
}
case 'layoutSection':
{
if (content) {
return {
type: type,
marks: marks,
content: content
};
}
break;
}
case 'layoutColumn':
{
if (attrs && content) {
if (attrs.width > 0 && attrs.width <= 100) {
return {
type: type,
content: content,
attrs: attrs
};
}
}
break;
}
case 'decisionList':
{
return {
type: type,
content: content,
attrs: {
localId: attrs && attrs.localId || uuid()
}
};
}
case 'decisionItem':
{
return {
type: type,
content: content,
attrs: {
localId: attrs && attrs.localId || uuid(),
state: attrs && attrs.state || 'DECIDED'
}
};
}
case 'taskList':
{
return {
type: type,
content: content,
attrs: {
localId: attrs && attrs.localId || uuid()
}
};
}
case 'taskItem':
{
return {
type: type,
content: content,
attrs: {
localId: attrs && attrs.localId || uuid(),
state: attrs && attrs.state || 'TODO'
}
};
}
case 'table':
{
if (Array.isArray(content) && content.length > 0 && !content.some(function (e) {
return e.type !== 'tableRow';
})) {
if (adfStage === 'stage0') {
return {
type: type,
content: content,
attrs: _objectSpread(_objectSpread({}, attrs), {}, {
localId: (attrs === null || attrs === void 0 ? void 0 : attrs.localId) || uuid(),
width: (attrs === null || attrs === void 0 ? void 0 : attrs.width) || null
})
};
}
return {
type: type,
content: content,
attrs: attrs
};
}
break;
}
case 'tableRow':
{
if (Array.isArray(content) && content.length > 0 && !content.some(function (e) {
return e.type !== 'tableCell' && e.type !== 'tableHeader';
})) {
return {
type: type,
content: content
};
}
break;
}
case 'tableCell':
case 'tableHeader':
{
if (content) {
var cellAttrs = {};
if (attrs) {
if (attrs.colspan && attrs.colspan > 1) {
cellAttrs.colspan = attrs.colspan;
}
if (attrs.rowspan && attrs.rowspan > 1) {
cellAttrs.rowspan = attrs.rowspan;
}
if (attrs.background) {
cellAttrs.background = attrs.background;
}
if (attrs.colwidth && Array.isArray(attrs.colwidth)) {
cellAttrs.colwidth = attrs.colwidth;
}
}
return {
type: type,
content: wrapInlineNodes(content),
attrs: attrs ? cellAttrs : undefined
};
}
break;
}
case 'image':
{
if (attrs && attrs.src) {
return {
type: type,
attrs: attrs
};
}
break;
}
case 'placeholder':
{
if (attrs && typeof attrs.text !== 'undefined') {
return {
type: type,
attrs: attrs
};
}
break;
}
case 'expand':
case 'nestedExpand':
{
return {
type: type,
attrs: attrs,
content: content,
marks: marks
};
}
}
}
return getValidUnknownNode(node);
};
/*
* This method will validate a Mark according to the spec defined here
* https://developer.atlassian.com/platform/atlassian-document-format/concepts/document-structure/marks/overview/
* This is also the place to handle backwards compatibility.
*
* If a node is not recognized or is missing required attributes, we should return null
*
*/
export var getValidMark = function getValidMark(mark) {
var adfStage = arguments.length > 1 && arguments[1] !== undefined ? arguments[1] : 'final';
var attrs = mark.attrs,
type = mark.type;
if (type) {
switch (type) {
case 'code':
{
return {
type: type
};
}
case 'em':
{
return {
type: type
};
}
case 'link':
{
if (attrs) {
var href = attrs.href,
url = attrs.url,
__confluenceMetadata = attrs.__confluenceMetadata;
var linkHref = href || url;
if (linkHref && linkHref.indexOf(':') === -1 && !RELATIVE_LINK.test(linkHref) && !ANCHOR_LINK.test(linkHref)) {
linkHref = "http://".concat(linkHref);
}
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
var linkAttrs = {
href: linkHref
};
if (__confluenceMetadata) {
linkAttrs.__confluenceMetadata = __confluenceMetadata;
}
if (linkHref && isSafeUrl(linkHref)) {
return {
type: type,
attrs: linkAttrs
};
}
}
break;
}
case 'strike':
{
return {
type: type
};
}
case 'strong':
{
return {
type: type
};
}
case 'subsup':
{
if (attrs && attrs['type']) {
var subSupType = attrs['type'];
if (isSubSupType(subSupType)) {
return {
type: type,
attrs: {
type: subSupType
}
};
}
}
break;
}
case 'textColor':
{
if (attrs && TEXT_COLOR_PATTERN.test(attrs.color)) {
return {
type: type,
attrs: attrs
};
}
break;
}
case 'underline':
{
return {
type: type
};
}
case 'annotation':
{
return {
type: type,
attrs: attrs
};
}
case 'border':
{
return {
type: type,
attrs: attrs
};
}
case 'backgroundColor':
{
if (attrs && TEXT_COLOR_PATTERN.test(attrs.color)) {
return {
type: type,
attrs: attrs
};
}
break;
}
}
}
if (adfStage === 'stage0') {
switch (type) {
case 'confluenceInlineComment':
{
return {
type: type,
attrs: attrs
};
}
case 'dataConsumer':
{
return {
type: type,
attrs: attrs
};
}
case 'fragment':
{
return {
type: type,
attrs: attrs
};
}
case 'border':
{
return {
type: type,
attrs: attrs
};
}
}
}
return null;
};