UNPKG

@atlaskit/editor-common

Version:

A package that contains common classes and components for editor and renderer

996 lines (982 loc) • 28.2 kB
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; };