@atlaskit/renderer
Version:
Renderer component
239 lines (227 loc) • 8.94 kB
JavaScript
/* eslint-disable @atlaskit/editor/no-re-export */
// Entry file in package.json
import { defaultSchema } from '@atlaskit/adf-schema/schema-default';
import { nativeEmbedsFallbackTransform, transformContainerNodes, transformMediaLinkMarks, transformNestedTablesIncomingDocument } from '@atlaskit/adf-utils/transforms';
import { ACTION, ACTION_SUBJECT, EVENT_TYPE } from '@atlaskit/editor-common/analytics';
import { findAndTrackUnsupportedContentNodes, validateADFEntity } from '@atlaskit/editor-common/utils';
import { getValidDocument } from '@atlaskit/editor-common/validator';
import { fg } from '@atlaskit/platform-feature-flags';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import memoizeOne from 'memoize-one';
import { PLATFORM } from './analytics/events';
import { trackUnsupportedContentLevels } from './analytics/unsupported-content';
import { countNodes } from './ui/Renderer/count-nodes';
const SUPPORTS_HIRES_TIMER_API = !!(typeof window !== 'undefined' && window.performance && performance.now);
const withStopwatch = cb => {
const startTime = SUPPORTS_HIRES_TIMER_API ? performance.now() : Date.now();
const output = cb();
const endTime = SUPPORTS_HIRES_TIMER_API ? performance.now() : Date.now();
const time = endTime - startTime;
return {
output,
time
};
};
const _validation = (doc, schema, adfStage, useSpecBasedValidator, dispatchAnalyticsEvent, skipValidation, validationOverrides) => {
let result;
if (useSpecBasedValidator) {
// link mark on mediaSingle is deprecated, need to move link mark to child media node
// https://product-fabric.atlassian.net/browse/ED-14043
const {
transformedAdf,
isTransformed
} = transformMediaLinkMarks(doc);
if (isTransformed && dispatchAnalyticsEvent) {
dispatchAnalyticsEvent({
action: ACTION.MEDIA_LINK_TRANSFORMED,
actionSubject: ACTION_SUBJECT.RENDERER,
eventType: EVENT_TYPE.OPERATIONAL
});
}
result = skipValidation ? transformedAdf || doc : validateADFEntity(schema, transformedAdf || doc, dispatchAnalyticsEvent, validationOverrides);
} else {
result = getValidDocument(doc, schema, adfStage);
}
if (!result) {
return result;
}
// ProseMirror always require a child under doc
if (result.type === 'doc' && useSpecBasedValidator) {
if (Array.isArray(result.content) && result.content.length === 0) {
result.content.push({
type: 'paragraph',
content: []
});
}
// Just making sure doc is always valid
if (!result.version) {
result.version = 1;
}
}
// Convert nested-table extensions into nested tables
try {
const {
transformedAdf,
isTransformed
} = transformNestedTablesIncomingDocument(result);
if (isTransformed) {
dispatchAnalyticsEvent === null || dispatchAnalyticsEvent === void 0 ? void 0 : dispatchAnalyticsEvent({
action: ACTION.NESTED_TABLE_TRANSFORMED,
actionSubject: ACTION_SUBJECT.RENDERER,
eventType: EVENT_TYPE.OPERATIONAL
});
result = transformedAdf;
}
} catch (e) {
dispatchAnalyticsEvent === null || dispatchAnalyticsEvent === void 0 ? void 0 : dispatchAnalyticsEvent({
action: ACTION.INVALID_PROSEMIRROR_DOCUMENT,
actionSubject: ACTION_SUBJECT.RENDERER,
eventType: EVENT_TYPE.OPERATIONAL,
attributes: {
platform: PLATFORM.WEB,
errorStack: `${e instanceof Error && e.name === 'NodeNestingTransformError' ? 'NodeNestingTransformError - Failed to encode one or more nested tables' : undefined}`
}
});
}
// Upgrade panel → panel_c1 where the schema allows it
if (result && expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true)) {
try {
const {
transformedAdf,
isTransformed
} = transformContainerNodes(result, schema);
if (isTransformed && transformedAdf) {
// TODO: EDITOR-7175 - Add analytics
result = transformedAdf;
}
} catch (e) {
// TODO: EDITOR-7175 - Add analytics
}
}
if (result && !expValEquals('cc-maui-experiment', 'isEnabled', true) && !fg('platform_native_embeds_rollout_non_maui_experience') && fg('platform_editor_native_embeds_fallback_transform')) {
const {
transformedAdf,
hasValidTransform
} = nativeEmbedsFallbackTransform(result, schema);
if (hasValidTransform && transformedAdf) {
dispatchAnalyticsEvent === null || dispatchAnalyticsEvent === void 0 ? void 0 : dispatchAnalyticsEvent({
action: ACTION.NATIVE_EMBEDS_TRANSFORMED,
actionSubject: ACTION_SUBJECT.RENDERER,
eventType: EVENT_TYPE.OPERATIONAL
});
result = transformedAdf;
}
}
return result;
};
const memoValidation = memoizeOne(_validation, (newArgs, lastArgs) => {
const [newDoc, newSchema, newADFStage, newUseSpecValidator,, /* ignoring dispatchAnalyticsEvent */newSkipValidation, newValidationOverrides] = newArgs;
const [oldDoc, oldSchema, oldADFStage, oldUseSpecValidator,, /* ignoring dispatchAnalyticsEvent */oldSkipValidation, oldValidationOverrides] = lastArgs;
return areDocsEqual(newDoc, oldDoc) && newSchema === oldSchema && newADFStage === oldADFStage && newUseSpecValidator === oldUseSpecValidator && newSkipValidation === oldSkipValidation && newValidationOverrides === oldValidationOverrides;
});
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const areDocsEqual = (docA, docB) => {
if (docA === docB) {
return true;
}
if (typeof docA === 'string' && typeof docB === 'string') {
return docA === docB;
}
// PMNode
if (docA.type && docA.toJSON && docB.type && docB.toJSON) {
return JSON.stringify(docA.toJSON()) === JSON.stringify(docB.toJSON());
}
// Object
return JSON.stringify(docA) === JSON.stringify(docB);
};
const _serializeFragment = (serializer, doc) => {
return serializer.serializeFragment(doc.content);
};
const memoSerializeFragment = memoizeOne(_serializeFragment, (newArgs, lastArgs) => {
const [newSerializer, newDoc] = newArgs;
const [oldSerializer, oldDoc] = lastArgs;
return newSerializer === oldSerializer && areDocsEqual(newDoc, oldDoc);
});
const _createNodeAndCheck = (schema, doc, dispatchAnalyticsEvent) => {
const pmNode = schema.nodeFromJSON(doc);
try {
pmNode.check();
} catch (err) {
if (dispatchAnalyticsEvent) {
dispatchAnalyticsEvent({
action: ACTION.INVALID_PROSEMIRROR_DOCUMENT,
actionSubject: ACTION_SUBJECT.RENDERER,
attributes: {
platform: PLATFORM.WEB,
errorStack: err instanceof Error ? err.message : String(err)
},
eventType: EVENT_TYPE.OPERATIONAL
});
}
}
return pmNode;
};
const memoCreateNodeAndCheck = memoizeOne(_createNodeAndCheck, (newArgs, lastArgs) => {
// ignore dispatchAnalyticsEvent
const [newSchema, newDoc] = newArgs;
const [oldSchema, oldDoc] = lastArgs;
return newSchema === oldSchema && areDocsEqual(newDoc, oldDoc);
});
export const renderDocument = (doc, serializer, schema = defaultSchema, adfStage = 'final', useSpecBasedValidator = false, rendererId = 'noid', dispatchAnalyticsEvent, unsupportedContentLevelsTracking, appearance, includeNodesCountInStats, skipValidation, validationOverrides) => {
const stat = {
sanitizeTime: 0
};
if (fg('platform_editor_renderer_rm_usespecbasedvalidator')) {
useSpecBasedValidator = true;
}
const {
output: validDoc,
time: sanitizeTime
} = withStopwatch(() => {
return memoValidation(doc, schema, adfStage, useSpecBasedValidator, dispatchAnalyticsEvent, skipValidation, validationOverrides);
});
// save sanitize time to stats
stat.sanitizeTime = sanitizeTime;
if (!validDoc) {
return {
stat,
result: null
};
}
const {
output: node,
time: buildTreeTime
} = withStopwatch(() => {
return memoCreateNodeAndCheck(schema, validDoc, dispatchAnalyticsEvent);
});
// save build tree time to stats
stat.buildTreeTime = buildTreeTime;
const {
output: result,
time: serializeTime
} = withStopwatch(() => {
return memoSerializeFragment(serializer, node);
});
// save serialize tree time to stats
stat.serializeTime = serializeTime;
if (dispatchAnalyticsEvent && useSpecBasedValidator) {
findAndTrackUnsupportedContentNodes(node, schema, dispatchAnalyticsEvent);
if (unsupportedContentLevelsTracking !== null && unsupportedContentLevelsTracking !== void 0 && unsupportedContentLevelsTracking.enabled) {
const documentData = {
doc: validDoc,
appearance,
rendererId
};
trackUnsupportedContentLevels(documentData, unsupportedContentLevelsTracking, dispatchAnalyticsEvent);
}
}
if (includeNodesCountInStats) {
stat.nodesCount = countNodes(doc);
}
return {
result,
stat,
pmDoc: node
};
};