@atlaskit/editor-core
Version:
A package contains Atlassian editor core functionality
559 lines • 19.2 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
var utils_1 = require("../plugins/utils");
var utils_2 = require("./utils");
var schema_1 = require("../schema");
/*
* It's important that this order follows the marks rank defined here:
* https://product-fabric.atlassian.net/wiki/spaces/E/pages/11174043/Document+structure#Documentstructure-Rank
*/
exports.markOrder = [
'link',
'em',
'strong',
'strike',
'subsup',
'underline',
'code',
];
exports.isSubSupType = function (type) {
return type === 'sub' || type === 'sup';
};
/*
* Sorts mark by the predefined order above
*/
exports.getMarksByOrder = function (marks) {
return marks.slice().sort(function (a, b) { return exports.markOrder.indexOf(a.type.name) - exports.markOrder.indexOf(b.type.name); });
};
/*
* Check if two marks are the same by comparing type and attrs
*/
exports.isSameMark = function (mark, otherMark) {
if (!mark || !otherMark) {
return false;
}
return mark.eq(otherMark);
};
exports.getValidDocument = function (doc, schema) {
if (schema === void 0) { schema = schema_1.defaultSchema; }
var node = exports.getValidNode(doc, schema);
if (node.type === 'doc') {
return node;
}
return null;
};
exports.getValidContent = function (content, schema) {
if (schema === void 0) { schema = schema_1.defaultSchema; }
return content.map(function (node) { return exports.getValidNode(node, schema); });
};
var TEXT_COLOR_PATTERN = /^#[0-9a-f]{6}$/i;
var flattenUnknownBlockTree = function (node, schema) {
if (schema === void 0) { schema = schema_1.defaultSchema; }
var output = [];
var isPrevLeafNode = false;
for (var i = 0; i < node.content.length; i++) {
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(exports.getValidNode(childNode, schema));
}
else {
output.push.apply(output, flattenUnknownBlockTree(childNode, schema));
}
isPrevLeafNode = isLeafNode;
}
return output;
};
// null is Object, also maybe check obj.constructor == Object if we want to skip Class
var isValidObject = function (obj) { return obj !== null && typeof obj === 'object'; };
var isValidString = function (str) { return typeof str === 'string'; };
var keysLen = function (obj) { return Object.keys(obj).length; };
var isValidIcon = function (icon) { return isValidObject(icon) && keysLen(icon) === 2 &&
isValidString(icon.url) && isValidString(icon.label); };
var isValidUser = function (user) {
var len = keysLen(user);
return isValidObject(user) && len <= 2 && isValidIcon(user.icon) && (len === 1 || isValidString(user.id));
};
/**
* Sanitize unknown node tree
*
* @see https://product-fabric.atlassian.net/wiki/spaces/E/pages/11174043/Document+structure#Documentstructure-ImplementationdetailsforHCNGwebrenderer
*/
exports.getValidUnknownNode = function (node) {
var _a = node.attrs, attrs = _a === void 0 ? {} : _a, content = node.content, text = node.text, type = node.type;
if (!content || !content.length) {
var unknownInlineNode = {
type: 'text',
text: text || attrs.text || "[" + type + "]",
};
if (attrs.textUrl) {
unknownInlineNode.marks = [{
type: 'link',
attrs: {
href: attrs.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),
};
};
/*
* 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'
*
*/
exports.getValidNode = function (originalNode, schema) {
if (schema === void 0) { schema = schema_1.defaultSchema; }
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 = exports.getValidContent(content, schema);
}
// If node type doesn't exist in schema, make it an unknown node
if (!schema.nodes[type]) {
return exports.getValidUnknownNode(node);
}
if (type) {
switch (type) {
case 'applicationCard': {
if (!attrs) {
break;
}
var text_1 = attrs.text, link = attrs.link, background = attrs.background, preview = attrs.preview, title = attrs.title, description = attrs.description, details = attrs.details, context_1 = attrs.context;
if (!isValidString(text_1) || !isValidObject(title) || !title.text) {
break;
}
// title can contain at most two keys (text, user)
var titleKeys = Object.keys(title);
if (titleKeys.length > 2) {
break;
}
if (titleKeys.length === 2 && !title.user) {
break;
}
if (title.user && !isValidUser(title.user)) {
break;
}
if ((link && !link.url) ||
(background && !background.url) ||
(preview && !preview.url) ||
(description && !description.text)) {
break;
}
if (context_1 && !isValidString(context_1.text)) {
break;
}
if (context_1 && !isValidIcon(context_1.icon)) {
break;
}
if (details && !Array.isArray(details)) {
break;
}
if (details && details.some(function (meta) {
var badge = meta.badge, lozenge = meta.lozenge, users = meta.users;
if (badge && !badge.value) {
return true;
}
if (lozenge && !lozenge.text) {
return true;
}
if (users && !Array.isArray(users)) {
return true;
}
if (users && !users.every(isValidUser)) {
return true;
}
})) {
break;
}
return {
type: type,
text: text_1,
attrs: attrs
};
}
case 'doc': {
var version = originalNode.version;
if (version && content && content.length) {
return {
type: type,
content: content
};
}
break;
}
case 'codeBlock': {
if (attrs && attrs.language !== undefined) {
return {
type: type,
attrs: attrs,
content: content
};
}
break;
}
case 'emoji': {
if (attrs && attrs.shortName) {
return {
type: type,
attrs: attrs
};
}
break;
}
case 'hardBreak': {
return {
type: type
};
}
case 'media': {
var mediaId = '';
var mediaType = '';
var mediaCollection = [];
if (attrs) {
var id = attrs.id, collection = attrs.collection, type_1 = attrs.type;
mediaId = id;
mediaType = type_1;
mediaCollection = collection;
}
if (mediaId && mediaType) {
return {
type: type,
attrs: {
type: mediaType,
id: mediaId,
collection: mediaCollection
}
};
}
break;
}
case 'mediaGroup': {
if (Array.isArray(content) && !content.some(function (e) { return e.type !== 'media'; })) {
return {
type: type,
content: content
};
}
break;
}
case 'mention': {
var mentionText = '';
var mentionId = void 0;
var mentionAccess = void 0;
if (attrs) {
var text_2 = attrs.text, displayName = attrs.displayName, id = attrs.id, accessLevel = attrs.accessLevel;
mentionText = text_2 || displayName;
mentionId = id;
mentionAccess = accessLevel;
}
if (!mentionText) {
mentionText = text || '@unknown';
}
if (mentionText && mentionId) {
var mentionNode = {
type: type,
attrs: {
id: mentionId,
text: mentionText
}
};
if (mentionAccess) {
mentionNode.attrs['accessLevel'] = mentionAccess;
}
return mentionNode;
}
break;
}
case 'paragraph': {
if (content) {
return {
type: type,
content: content
};
}
break;
}
case 'rule': {
return {
type: type,
};
}
case 'text': {
var marks_1 = node.marks;
if (text) {
if (marks_1) {
marks_1 = marks_1.reduce(function (acc, mark) {
var validMark = exports.getValidMark(mark);
if (validMark) {
acc.push(validMark);
}
return acc;
}, []);
}
return marks_1 ? { type: type, text: text, marks: marks_1 } : { type: type, text: text };
}
break;
}
case 'heading': {
if (attrs && content) {
var level = attrs.level;
var between = function (x, a, b) { return x >= a && x <= b; };
if (level && between(level, 1, 6)) {
return {
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: content,
};
}
break;
}
case 'blockquote': {
if (content) {
return {
type: type,
content: content,
};
}
break;
}
case 'panel': {
var types = ['info', 'note', 'tip', 'warning'];
if (attrs && content) {
var panelType = attrs.panelType;
if (types.indexOf(panelType) > -1) {
return {
type: type,
attrs: { panelType: panelType },
content: content,
};
}
}
break;
}
case 'decisionList': {
return {
type: type,
content: content,
attrs: {
localId: attrs && attrs.localId || utils_1.uuid(),
},
};
}
case 'decisionItem': {
return {
type: type,
content: content,
attrs: {
localId: attrs && attrs.localId || utils_1.uuid(),
state: attrs && attrs.state || 'DECIDED'
},
};
}
case 'taskList': {
return {
type: type,
content: content,
attrs: {
localId: attrs && attrs.localId || utils_1.uuid()
},
};
}
case 'taskItem': {
return {
type: type,
content: content,
attrs: {
localId: attrs && attrs.localId || utils_1.uuid(),
state: attrs && attrs.state || 'TODO'
},
};
}
case 'table': {
if (Array.isArray(content)
&& content.length > 0
&& !content.some(function (e) { return e.type !== 'tableRow'; })) {
return {
type: type,
content: content
};
}
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': {
if (content) {
return {
type: type,
content: content
};
}
break;
}
case 'tableHeader': {
if (content) {
return {
type: type,
content: content
};
}
break;
}
}
}
return exports.getValidUnknownNode(node);
};
/*
* This method will validate a Mark according to the spec defined here
* https://product-fabric.atlassian.net/wiki/spaces/E/pages/11174043/Document+structure#Documentstructure-Marks
*
* This is also the place to handle backwards compatibility.
*
* If a node is not recognized or is missing required attributes, we should return null
*
*/
exports.getValidMark = function (mark) {
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;
var linkHref = href || url;
if (linkHref.indexOf(':') === -1) {
linkHref = "http://" + linkHref;
}
if (linkHref && utils_2.isSafeUrl(linkHref)) {
return {
type: type,
attrs: {
href: linkHref
}
};
}
}
break;
}
case 'strike': {
return {
type: type,
};
}
case 'strong': {
return {
type: type,
};
}
case 'subsup': {
if (attrs && attrs['type']) {
var subSupType = attrs['type'];
if (exports.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,
};
}
}
}
return null;
};
//# sourceMappingURL=validator.js.map