@atlaskit/adf-schema
Version:
Shared package that contains the ADF-schema (json) and ProseMirror node/mark specs
320 lines (314 loc) • 7.92 kB
JavaScript
import { Schema } from 'prosemirror-model';
import { COLOR, FONT_STYLE, SEARCH_QUERY, LINK } from './groups';
import { link, em, strong, textColor, strike, subsup, underline, code, typeAheadQuery, confluenceInlineComment, breakout, alignment, indentation, annotation, unsupportedMark, unsupportedNodeAttribute, dataConsumer, fragment, border } from './marks';
import { confluenceJiraIssue, confluenceUnsupportedBlock, confluenceUnsupportedInline, doc, paragraph, text, bulletList, orderedListWithOrder, listItem, heading, blockquote, codeBlock, panel, rule, image, mention, media, mediaInline, mediaGroup, mediaSingleWithCaption, hardBreak, emoji, table, tableCell, tableHeader, tableRow, decisionList, decisionItem, taskList, taskItem, unknownBlock, extension, inlineExtension, bodiedExtension, date, placeholder, layoutSection, layoutColumn, inlineCard, blockCard, unsupportedBlock, unsupportedInline, status, expand, nestedExpand, embedCard, caption } from './nodes';
function addItems(builtInItems, config, customSpecs = {}) {
if (!config) {
return {};
}
/**
* Add built-in Node / Mark specs
*/
const items = builtInItems.reduce((items, {
name,
spec
}) => {
if (config.indexOf(name) !== -1) {
items[name] = customSpecs[name] || spec;
}
return items;
}, {});
/**
* Add Custom Node / Mark specs
*/
return Object.keys(customSpecs).reduce((items, name) => {
if (items[name]) {
return items;
}
items[name] = customSpecs[name];
return items;
}, items);
}
// We use groups to allow schemas to be constructed in different shapes without changing node/mark
// specs, but this means nodes/marks are defined with groups that might never be used in the schema.
// In this scenario ProseMirror will complain and prevent the schema from being constructed.
//
// To avoid the problem, we include items that serve to "declare" the groups in the schema. This
// approach unfortunately leaves unused items in the schema, but has the benefit of avoiding the
// need to manipulate `exclude` or content expression values for potentially every schema item.
function groupDeclaration(name) {
return {
name: `__${name}GroupDeclaration`,
spec: {
group: name
}
};
}
const markGroupDeclarations = [groupDeclaration(COLOR), groupDeclaration(FONT_STYLE), groupDeclaration(SEARCH_QUERY), groupDeclaration(LINK)];
const markGroupDeclarationsNames = markGroupDeclarations.map(groupMark => groupMark.name);
const nodesInOrder = [{
name: 'doc',
spec: doc
}, {
name: 'paragraph',
spec: paragraph
}, {
name: 'text',
spec: text
}, {
name: 'bulletList',
spec: bulletList
}, {
name: 'orderedList',
spec: orderedListWithOrder
}, {
name: 'listItem',
spec: listItem
}, {
name: 'heading',
spec: heading
}, {
name: 'blockquote',
spec: blockquote
}, {
name: 'codeBlock',
spec: codeBlock
}, {
name: 'panel',
spec: panel(true)
}, {
name: 'rule',
spec: rule
}, {
name: 'image',
spec: image
}, {
name: 'mention',
spec: mention
}, {
name: 'caption',
spec: caption
}, {
name: 'media',
spec: media
}, {
name: 'mediaGroup',
spec: mediaGroup
}, {
name: 'mediaSingle',
spec: mediaSingleWithCaption
}, {
name: 'mediaInline',
spec: mediaInline
}, {
name: 'placeholder',
spec: placeholder
}, {
name: 'layoutSection',
spec: layoutSection
}, {
name: 'layoutColumn',
spec: layoutColumn
}, {
name: 'hardBreak',
spec: hardBreak
}, {
name: 'emoji',
spec: emoji
}, {
name: 'table',
spec: table
}, {
name: 'tableCell',
spec: tableCell
}, {
name: 'tableRow',
spec: tableRow
}, {
name: 'tableHeader',
spec: tableHeader
}, {
name: 'confluenceJiraIssue',
spec: confluenceJiraIssue
}, {
name: 'confluenceUnsupportedInline',
spec: confluenceUnsupportedInline
}, {
name: 'confluenceUnsupportedBlock',
spec: confluenceUnsupportedBlock
}, {
name: 'decisionList',
spec: decisionList
}, {
name: 'decisionItem',
spec: decisionItem
}, {
name: 'taskList',
spec: taskList
}, {
name: 'taskItem',
spec: taskItem
}, {
name: 'date',
spec: date
}, {
name: 'status',
spec: status
}, {
name: 'expand',
spec: expand
}, {
name: 'nestedExpand',
spec: nestedExpand
}, {
name: 'extension',
spec: extension
}, {
name: 'inlineExtension',
spec: inlineExtension
}, {
name: 'bodiedExtension',
spec: bodiedExtension
}, {
name: 'inlineCard',
spec: inlineCard
}, {
name: 'blockCard',
spec: blockCard
}, {
name: 'embedCard',
spec: embedCard
}, {
name: 'unknownBlock',
spec: unknownBlock
}, {
name: 'unsupportedBlock',
spec: unsupportedBlock
}, {
name: 'unsupportedInline',
spec: unsupportedInline
}];
const marksInOrder = [{
name: 'link',
spec: link
}, {
name: 'em',
spec: em
}, {
name: 'strong',
spec: strong
}, {
name: 'textColor',
spec: textColor
}, {
name: 'strike',
spec: strike
}, {
name: 'subsup',
spec: subsup
}, {
name: 'underline',
spec: underline
}, {
name: 'code',
spec: code
}, {
name: 'typeAheadQuery',
spec: typeAheadQuery
}, {
name: 'alignment',
spec: alignment
}, {
name: 'annotation',
spec: annotation
}, {
name: 'confluenceInlineComment',
spec: confluenceInlineComment
}, ...markGroupDeclarations, {
name: 'breakout',
spec: breakout
}, {
name: 'dataConsumer',
spec: dataConsumer
}, {
name: 'fragment',
spec: fragment
}, {
name: 'indentation',
spec: indentation
}, {
name: 'border',
spec: border
}, {
name: 'unsupportedMark',
spec: unsupportedMark
}, {
name: 'unsupportedNodeAttribute',
spec: unsupportedNodeAttribute
}];
/**
* Creates a schema preserving order of marks and nodes.
*/
export function createSchema(config) {
const {
customNodeSpecs,
customMarkSpecs
} = config;
const nodesConfig = Object.keys(customNodeSpecs || {}).concat(config.nodes);
const marksConfig = Object.keys(customMarkSpecs || {}).concat(config.marks || []).concat(markGroupDeclarationsNames);
let nodes = addItems(nodesInOrder, nodesConfig, customNodeSpecs);
let marks = addItems(marksInOrder, marksConfig, customMarkSpecs);
nodes = sanitizeNodes(nodes, marks);
return new Schema({
nodes,
marks
});
}
export function sanitizeNodes(nodes, supportedMarks) {
const nodeNames = Object.keys(nodes);
nodeNames.forEach(nodeKey => {
const nodeSpec = {
...nodes[nodeKey]
};
if (nodeSpec.marks && nodeSpec.marks !== '_') {
nodeSpec.marks = nodeSpec.marks.split(' ').filter(mark => !!supportedMarks[mark]).join(' ');
}
if (nodeSpec.content) {
nodeSpec.content = sanitizeNodeSpecContent(nodes, nodeSpec.content);
}
nodes[nodeKey] = nodeSpec;
});
return nodes;
}
export function sanitizeNodeSpecContent(nodes, rawContent) {
const content = rawContent.replace(/\W/g, ' ');
const contentKeys = content.split(' ');
const unsupportedContentKeys = contentKeys.filter(contentKey => !isContentSupported(nodes, contentKey));
return unsupportedContentKeys.reduce((newContent, nodeName) => sanitizedContent(newContent, nodeName), rawContent);
}
function sanitizedContent(content, invalidContent) {
if (!invalidContent.length) {
return content || '';
}
if (!content || !content.match(/\w/)) {
return '';
}
const pattern = `(${invalidContent}((\\s)*\\|)+)|((\\|(\\s)*)+${invalidContent})|(${invalidContent}$)|(${invalidContent}(\\+|\\*))`;
return content.replace(new RegExp(pattern, 'g'), '').replace(' ', ' ').trim();
}
function isContentSupported(nodes, contentKey) {
const nodeKeys = Object.keys(nodes);
// content is with valid node
if (nodeKeys.indexOf(contentKey) > -1) {
return true;
}
// content is with valid group
for (const supportedKey in nodes) {
const nodeSpec = nodes[supportedKey];
if (nodeSpec && nodeSpec.group === contentKey) {
return true;
}
}
return false;
}
export const allowCustomPanel = true;