UNPKG

@atlaskit/editor-common

Version:

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

323 lines (316 loc) • 11.6 kB
import { syncBlockFallbackTransform, transformDedupeMarks, transformIndentationMarks, transformInvalidMediaContent, transformMediaLinkMarks, transformNestedTablesIncomingDocument, transformNodesMissingContent, transformTextLinkCodeMarks, transformMediaSingleWidth } from '@atlaskit/adf-utils/transforms'; import { Fragment, Node } from '@atlaskit/editor-prosemirror/model'; import { fg } from '@atlaskit/platform-feature-flags'; import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '../analytics'; import { isNestedTablesSupported, isPanelNestingTableSupported } from '../nesting/utilities'; import { sanitizeNodeForPrivacy } from './filter/privacy-filter'; import { findAndTrackUnsupportedContentNodes } from './track-unsupported-content'; import { validateADFEntity } from './validate-using-spec'; const transformNestedTablesWithAnalytics = (node, dispatchAnalyticsEvent) => { try { const { transformedAdf, isTransformed } = transformNestedTablesIncomingDocument(node); if (fg('platform_editor_show_diff_scroll_navigation')) { if (isTransformed) { if (dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.NESTED_TABLE_TRANSFORMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } return { transformedAdf, isTransformed }; } } else { if (isTransformed && dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.NESTED_TABLE_TRANSFORMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); return { transformedAdf, isTransformed }; } } } catch (e) { // eslint-disable-next-line no-console console.error('Failed to transform one or more nested tables in the document'); if (dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.DOCUMENT_PROCESSING_ERROR, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL, attributes: { errorMessage: `${e instanceof Error && e.name === 'NodeNestingTransformError' ? 'NodeNestingTransformError - Failed to transform one or more nested tables' : undefined}` } }); } } return { transformedAdf: node, isTransformed: false }; }; export function processRawValueWithoutValidation(schema, value, dispatchAnalyticsEvent) { if (!value) { return; } let node; if (typeof value === 'string') { try { node = JSON.parse(value); } catch { // eslint-disable-next-line no-console console.error(`Error processing value: ${value} isn't a valid JSON`); return; } } else { node = value; } // Convert nested-table extensions into nested tables let { transformedAdf } = transformNestedTablesWithAnalytics(node, dispatchAnalyticsEvent); const result = syncBlockFallbackTransform(schema, transformedAdf); if (result.isTransformed && result.transformedAdf) { transformedAdf = result.transformedAdf; } return Node.fromJSON(schema, transformedAdf); } export function processRawValue(schema, value, providerFactory, sanitizePrivateContent, contentTransformer, dispatchAnalyticsEvent) { if (!value) { return; } let node; if (typeof value === 'string') { try { if (contentTransformer) { const doc = contentTransformer.parse(value); node = doc.toJSON(); } else { node = JSON.parse(value); } } catch { // eslint-disable-next-line no-console console.error(`Error processing value: ${value} isn't a valid JSON`); return; } } else { node = value; } if (Array.isArray(node)) { // eslint-disable-next-line no-console console.error(`Error processing value: ${node} is an array, but it must be an object.`); return; } try { // ProseMirror always require a child under doc if (node.type === 'doc') { if (Array.isArray(node.content) && node.content.length === 0) { node.content.push({ type: 'paragraph', content: [] }); } // Just making sure doc is always valid if (!node.version) { node.version = 1; } } if (contentTransformer) { return Node.fromJSON(schema, node); } // link mark on mediaSingle is deprecated, need to move link mark to child media node // https://product-fabric.atlassian.net/browse/ED-14043 let { transformedAdf, isTransformed } = transformMediaLinkMarks(node); if (isTransformed && dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.MEDIA_LINK_TRANSFORMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } if (fg('platform_editor_transform_invalid_media_width')) { // Fix mediaSingle width issues ({ transformedAdf, isTransformed } = transformMediaSingleWidth(transformedAdf)); if (isTransformed && dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.MEDIA_SINGLE_WIDTH_TRANSFORMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } } // See: HOT-97965 https://product-fabric.atlassian.net/browse/ED-14400 // We declared in code mark spec that links and marks should not co-exist on // text nodes. This util strips code marks from bad text nodes and preserves links. // Otherwise, prosemirror will try to repair the invalid document by stripping links // and preserving code marks during content changes. ({ transformedAdf, isTransformed } = transformTextLinkCodeMarks(transformedAdf)); if (isTransformed && dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.TEXT_LINK_MARK_TRANSFORMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } let discardedMarks = []; ({ transformedAdf, isTransformed, discardedMarks } = transformDedupeMarks(transformedAdf)); if (isTransformed && dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.DEDUPE_MARKS_TRANSFORMED_V2, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL, attributes: { /** UGC WARNING * * DO NOT include the `mark` attributes inside, we map here to only * extract the mark type as that is the only non-UGC safe information * that we can add to event-attributes * */ discardedMarkTypes: discardedMarks.map(mark => mark.type) } }); } ({ transformedAdf, isTransformed } = transformNodesMissingContent(transformedAdf)); if (isTransformed && dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.NODES_MISSING_CONTENT_TRANSFORMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } ({ transformedAdf, isTransformed } = transformIndentationMarks(transformedAdf)); if (isTransformed && dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.INDENTATION_MARKS_TRANSFORMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } ({ transformedAdf, isTransformed } = transformInvalidMediaContent(transformedAdf)); if (isTransformed && dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.INVALID_MEDIA_CONTENT_TRANSFORMED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } if (dispatchAnalyticsEvent) { var _transformedAdf$conte; const hasSingleColumnLayout = (_transformedAdf$conte = transformedAdf.content) === null || _transformedAdf$conte === void 0 ? void 0 : _transformedAdf$conte.some(node => { var _node$content; return node && node.type === 'layoutSection' && ((_node$content = node.content) === null || _node$content === void 0 ? void 0 : _node$content.length) === 1; }); if (hasSingleColumnLayout) { dispatchAnalyticsEvent({ action: ACTION.SINGLE_COL_LAYOUT_DETECTED, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } } // Validate ADF first before converting nested-table extensions into nested tables // This matches the renderer's behavior in render-document.ts const allowNestedTables = isNestedTablesSupported(schema); const allowTableInPanel = isPanelNestingTableSupported(schema); const validateADFEntityOptions = allowNestedTables || allowTableInPanel ? { allowNestedTables: allowNestedTables || undefined, allowTableInPanel: allowTableInPanel || undefined } : undefined; let entity = validateADFEntity(schema, transformedAdf || node, dispatchAnalyticsEvent, validateADFEntityOptions); // Convert nested-table extensions into nested tables ({ transformedAdf } = transformNestedTablesWithAnalytics(entity, dispatchAnalyticsEvent)); entity = transformedAdf; const newEntity = maySanitizePrivateContent(entity, providerFactory, sanitizePrivateContent); const parsedDoc = Node.fromJSON(schema, newEntity); // throws an error if the document is invalid try { parsedDoc.check(); } catch (err) { if (dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.INVALID_PROSEMIRROR_DOCUMENT, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } throw err; } if (dispatchAnalyticsEvent) { findAndTrackUnsupportedContentNodes(parsedDoc, schema, dispatchAnalyticsEvent); } return parsedDoc; } catch (e) { if (dispatchAnalyticsEvent) { dispatchAnalyticsEvent({ action: ACTION.DOCUMENT_PROCESSING_ERROR, actionSubject: ACTION_SUBJECT.EDITOR, eventType: EVENT_TYPE.OPERATIONAL }); } // eslint-disable-next-line no-console console.error(`Error processing document:\n${e instanceof Error ? e.message : String(e)}\n\n`, JSON.stringify(node)); if (isProseMirrorSchemaCheckError(e)) { throw e; } return; } } export function processRawFragmentValue(schema, value, providerFactory, sanitizePrivateContent, contentTransformer, dispatchAnalyticsEvent) { if (!value) { return; } const adfEntities = value.map(item => processRawValue(schema, item, providerFactory, sanitizePrivateContent, contentTransformer, dispatchAnalyticsEvent)).filter(item => Boolean(item)); if (adfEntities.length === 0) { return; } return Fragment.from(adfEntities); } function isProseMirrorSchemaCheckError(error) { return error instanceof RangeError && ( // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp !!error.message.match(/^Invalid collection of marks for node/) || // Ignored via go/ees005 // eslint-disable-next-line require-unicode-regexp !!error.message.match(/^Invalid content for node/)); } const maySanitizePrivateContent = (entity, providerFactory, sanitizePrivateContent) => { if (sanitizePrivateContent && providerFactory) { return sanitizeNodeForPrivacy(entity, providerFactory); } return entity; };