@atlaskit/renderer
Version:
Renderer component
739 lines (734 loc) • 29.6 kB
JavaScript
import _extends from "@babel/runtime/helpers/extends";
import _defineProperty from "@babel/runtime/helpers/defineProperty";
import React from 'react';
import { MarkType } from '@atlaskit/editor-prosemirror/model';
import { expValEquals } from '@atlaskit/tmp-editor-statsig/exp-val-equals';
import { editorExperiment } from '@atlaskit/tmp-editor-statsig/experiments';
import { Doc, DocWithSelectAllTrap, isTextNode, isTextWrapper, mergeTextNodes, toReact } from './nodes';
import TextWrapperComponent from './nodes/text-wrapper';
import { isNestedHeaderLinksEnabled } from './utils/links';
import { getColumnWidths } from '@atlaskit/editor-common/utils';
import { getMarksByOrder, isSameMark } from '@atlaskit/editor-common/validator';
import { findChildrenByMark, findChildrenByType } from '@atlaskit/editor-prosemirror/utils';
import { fg } from '@atlaskit/platform-feature-flags';
import { getText } from '../utils';
import { isAnnotationMark, toReact as markToReact } from './marks';
import { isCodeMark } from './marks/code';
import { getNestedUnderNodes, insideBlockNode, insideBreakoutExpand, insideBreakoutLayout, insideMultiBodiedExtension, insideTable } from './renderer-node';
import { renderTextSegments } from './utils/render-text-segments';
import { segmentText } from './utils/segment-text';
import { getStandaloneBackgroundColorMarks } from './utils/getStandaloneBackgroundColorMarks';
import { markBlockAsInline } from './utils/markBlockAsInline';
function mergeMarks(marksAndNodes) {
return marksAndNodes.reduce((acc, markOrNode) => {
const prev = acc.length && acc[acc.length - 1] || null;
if (markOrNode.type instanceof MarkType && prev && prev.type instanceof MarkType && Array.isArray(prev.content) && isSameMark(prev, markOrNode)) {
prev.content = mergeMarks(prev.content.concat(markOrNode.content));
} else {
acc.push(markOrNode);
}
return acc;
}, []);
}
export default class ReactSerializer {
constructor(init) {
_defineProperty(this, "headingIds", []);
/**
* The reason we have this extra array here is because we need to generate the same unique
* heading id for 2 different nodes: headers and expands (check the implementation of
* `getUniqueHeadingId` for more info).
*
* We will eventually need to refactor the current approach to generate unique ids
* for headers under this ticket -> https://product-fabric.atlassian.net/browse/ED-9668
*/
_defineProperty(this, "expandHeadingIds", []);
_defineProperty(this, "allowCopyToClipboard", false);
_defineProperty(this, "allowWrapCodeBlock", false);
_defineProperty(this, "allowPlaceholderText", true);
_defineProperty(this, "allowCustomPanels", false);
_defineProperty(this, "surroundTextNodesWithTextWrapper", false);
_defineProperty(this, "allowAnnotations", false);
_defineProperty(this, "standaloneBackgroundColorMarks", []);
_defineProperty(this, "inlinePositions", new Set());
_defineProperty(this, "serializeFragmentChild", (node, {
index,
parentInfo
}) => {
const pos = this.startPos;
const currentPath = parentInfo && parentInfo.path || [];
const parentIsIncompleteTask = (node.type.name === 'taskItem' || node.type.name === 'blockTaskItem' && expValEquals('platform_editor_blocktaskitem_node_tenantid', 'isEnabled', true)) && node.attrs.state !== 'DONE' ||
// If blockTaskItem is in the schema, check if the parent of the current node
// is an incomplete task item, because blockTaskItems can have nested nodes
node.type.schema.nodes.blockTaskItem && (parentInfo === null || parentInfo === void 0 ? void 0 : parentInfo.parentIsIncompleteTask) === true && expValEquals('platform_editor_blocktaskitem_node_tenantid', 'isEnabled', true);
const nodeKey = `${node.type.name}__${this.startPos}`;
const serializedContent = this.serializeFragment(node.content, this.getNodeProps(node, parentInfo), toReact(node, {
allowSelectAllTrap: this.allowSelectAllTrap,
allowWindowedCodeBlock: this.allowWindowedCodeBlock
}, this.nodeComponents), nodeKey, {
parentIsIncompleteTask,
path: [...currentPath, node],
pos: this.startPos
});
this.startPos = pos + node.nodeSize;
const marks = node.marks ? [...node.marks] : [];
const isMedia = node.type.name === 'media';
const shouldSkipBorderMark = mark => currentPath.some(n => {
var _n$type;
return ((_n$type = n.type) === null || _n$type === void 0 ? void 0 : _n$type.name) !== 'mediaSingle';
}) && isMedia && mark.type.name === 'border';
const shouldSkipLinkMark = mark => this.allowMediaLinking !== true && isMedia && mark.type.name === 'link';
return marks.reduceRight((content, mark) => {
if (shouldSkipLinkMark(mark) || shouldSkipBorderMark(mark)) {
return content;
}
return this.renderMark(markToReact(mark), this.withMediaMarkProps(node, mark, this.getMarkProps(mark, [], node)), `${mark.type.name}-${index}`, content);
}, serializedContent);
});
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
_defineProperty(this, "withMediaMarkProps", (node, mark, defaultProps) => {
if (mark.type.name === 'link' && node.type.name === 'media') {
return {
...defaultProps,
isMediaLink: true
};
}
if (node.type.name === 'mediaInline' && mark.type.name === 'annotation') {
return {
...defaultProps,
isMediaInline: true
};
}
return defaultProps;
});
_defineProperty(this, "getUnsupportedContentProps", node => {
return {
node,
dispatchAnalyticsEvent: this.fireAnalyticsEvent
};
});
_defineProperty(this, "getAnnotationMarkProps", (mark, marksParentPath) => {
const annotationParentIds = (marksParentPath || []).reduce((acc, parent) => {
if (isAnnotationMark(parent)) {
return [...acc, parent.attrs.id];
}
return acc;
}, []);
return {
id: mark.attrs.id,
annotationType: mark.attrs.annotationType,
annotationParentIds,
allowAnnotations: this.allowAnnotations,
dataAttributes: {
'data-renderer-mark': true
}
};
});
_defineProperty(this, "getMarkProps", (mark, marksParentPath, node) => {
if (isAnnotationMark(mark)) {
return this.getAnnotationMarkProps(mark, marksParentPath);
}
const {
key,
...otherAttrs
} = mark.attrs;
const extraProps = {
isInline: node === null || node === void 0 ? void 0 : node.isInline
};
// currently the only mark which has custom props is the code mark
const markSpecificProps = isCodeMark(mark) ? {
// The appearance being mobile indicates we are in an renderer being
// rendered by mobile bridge in a web view.
// The tooltip is likely to have unexpected behaviour there, with being cut
// off, so we disable it. This is also to keep the behaviour consistent with
// the rendering in the mobile Native Renderer.
codeBidiWarningTooltipEnabled: false
} : {};
// Add deepLinkTarget for link marks
const linkSpecificProps = mark.type.name === 'link' ? {
onSetLinkTarget: this.onSetLinkTarget
} : {};
const props = {
eventHandlers: this.eventHandlers,
fireAnalyticsEvent: this.fireAnalyticsEvent,
markKey: key,
...otherAttrs,
...extraProps,
...markSpecificProps,
...linkSpecificProps,
dataAttributes: {
'data-renderer-mark': true
}
};
return props;
});
if (editorExperiment('comment_on_bodied_extensions', true)) {
this.initStartPos = init.startPos || 1;
this.startPos = init.startPos || 1;
} else {
this.initStartPos = 1;
this.startPos = 1;
}
this.providers = init.providers;
this.eventHandlers = init.eventHandlers;
this.extensionHandlers = init.extensionHandlers;
this.portal = init.portal;
this.rendererContext = init.objectContext;
this.appearance = init.appearance;
this.contentMode = init.contentMode;
this.disableHeadingIDs = init.disableHeadingIDs;
this.disableActions = init.disableActions;
this.allowHeadingAnchorLinks = init.allowHeadingAnchorLinks;
this.allowCopyToClipboard = init.allowCopyToClipboard;
this.allowWrapCodeBlock = init.allowWrapCodeBlock;
this.allowPlaceholderText = init.allowPlaceholderText;
this.allowCustomPanels = init.allowCustomPanels;
this.allowColumnSorting = init.allowColumnSorting;
this.fireAnalyticsEvent = init.fireAnalyticsEvent;
this.shouldOpenMediaViewer = init.shouldOpenMediaViewer;
this.allowAltTextOnImages = init.allowAltTextOnImages;
this.stickyHeaders = init.stickyHeaders;
this.allowMediaLinking = init.allowMediaLinking;
this.allowAnnotations = Boolean(init.allowAnnotations);
this.surroundTextNodesWithTextWrapper = Boolean(init.surroundTextNodesWithTextWrapper);
this.media = init.media;
this.emojiResourceConfig = init.emojiResourceConfig;
this.smartLinks = init.smartLinks;
this.extensionViewportSizes = init.extensionViewportSizes;
this.getExtensionHeight = init.getExtensionHeight;
this.allowSelectAllTrap = init.allowSelectAllTrap;
this.nodeComponents = init.nodeComponents;
this.allowWindowedCodeBlock = init.allowWindowedCodeBlock;
this.isInsideOfInlineExtension = init.isInsideOfInlineExtension;
this.textHighlighter = init.textHighlighter;
this.allowTableAlignment = init.allowTableAlignment;
this.allowTableResizing = init.allowTableResizing;
this.allowFixedColumnWidthOption = init.allowFixedColumnWidthOption;
this.isPresentational = init.isPresentational;
this.disableTableOverflowShadow = init.disableTableOverflowShadow;
this.onSetLinkTarget = init.onSetLinkTarget;
this.shouldDisplayExtensionAsInline = init.shouldDisplayExtensionAsInline;
}
resetState() {
this.headingIds = [];
this.expandHeadingIds = [];
this.startPos = this.initStartPos;
}
getNodeProps(node, parentInfo) {
const path = parentInfo ? parentInfo.path : undefined;
switch (node.type.name) {
case 'date':
return this.getDateProps(node, parentInfo, path);
case 'hardBreak':
return this.getHardBreakProps(node, path);
case 'heading':
return this.getHeadingProps(node, path);
case 'media':
return this.getMediaProps(node, path);
case 'emoji':
return this.getEmojiProps(node, path);
case 'extension':
case 'bodiedExtension':
return this.getExtensionProps(node, path);
case 'mediaGroup':
return this.getMediaGroupProps(node);
case 'mediaInline':
return this.getMediaInlineProps(node);
case 'mediaSingle':
return this.getMediaSingleProps(node, path);
case 'table':
return this.getTableProps(node, path);
case 'tableHeader':
case 'tableRow':
return this.getTableChildrenProps(node);
case 'taskItem':
return this.getTaskItemProps(node, path);
case 'embedCard':
return this.getEmbedCardProps(node, path);
case 'blockCard':
return this.getBlockCardProps(node, path);
case 'inlineCard':
return this.getInlineCardProps(node, path);
case 'expand':
return this.getExpandProps(node, path);
case 'nestedExpand':
if (fg('hot-121622_lazy_load_expand_content')) {
return this.getExpandProps(node, path);
}
return this.getProps(node, path);
case 'unsupportedBlock':
case 'unsupportedInline':
return this.getUnsupportedContentProps(node);
case 'codeBlock':
return this.getCodeBlockProps(node);
case 'panel':
return this.getPanelProps(node);
case 'panel_c1':
if (expValEquals('platform_editor_nest_table_in_panel', 'isEnabled', true)) {
return this.getPanelProps(node);
}
return this.getProps(node, path);
default:
return this.getProps(node, path);
}
}
serializeFragment(fragment,
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props = {},
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
target = this.allowSelectAllTrap ? DocWithSelectAllTrap : Doc, key = 'root-0', parentInfo) {
// This makes sure that we reset internal state on re-render.
if (key === 'root-0') {
this.resetState();
}
return this.renderNode(target, props, key, this.getChildNodes(fragment).map((node, index) => {
if (isTextWrapper(node)) {
return this.serializeTextWrapper(node.content, {
index,
parentInfo
});
}
return this.serializeFragmentChild(node, {
index,
parentInfo
});
}));
}
serializeTextWrapper(content, {
parentInfo
}) {
const currentPath = parentInfo && parentInfo.path || [];
const nodePosition = parentInfo && parentInfo.pos || 1;
this.standaloneBackgroundColorMarks.push(...getStandaloneBackgroundColorMarks(content));
return ReactSerializer.buildMarkStructure(content).map((mark, _index) => {
return this.serializeMark({
mark,
parentNode: {
path: currentPath,
pos: nodePosition
},
parentMark: {
path: [mark]
}
});
});
}
serializeMark({
mark,
parentNode,
parentMark
}) {
if (!isTextNode(mark)) {
const serializeContent = (childMark, _index) => this.serializeMark({
mark: childMark,
parentNode,
parentMark: {
path: [...parentMark.path, childMark]
}
});
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const content = (mark.content || []).map(serializeContent);
const markKey = `${mark.type.name}-component__${this.startPos}__${parentMark.path.length}`;
const isStandalone = this.standaloneBackgroundColorMarks.some(m => mark.eq(m));
return this.renderMark(markToReact(mark), {
...this.getMarkProps(mark, parentMark.path),
isStandalone
}, markKey, content);
}
const startPos = this.startPos;
const endPos = startPos + mark.nodeSize;
this.startPos = endPos;
const textKey = `text-wrapper_${this.startPos}`;
if (this.surroundTextNodesWithTextWrapper) {
const parentDepth = Math.max(parentNode.path.length - 1, 0);
return /*#__PURE__*/React.createElement(TextWrapperComponent, {
key: textKey,
startPos: startPos + parentDepth,
endPos: endPos + parentDepth,
textHighlighter: this.textHighlighter,
marks: mark.marks
}, mark.text);
}
const segments = segmentText(mark.text, this.textHighlighter);
return renderTextSegments(segments, this.textHighlighter, mark.marks, startPos);
}
renderNode(
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
NodeComponent,
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
props, key,
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content) {
return (
/*#__PURE__*/
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
React.createElement(NodeComponent, _extends({
key: key
}, props), content)
);
}
renderMark(
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
MarkComponent, props, key,
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
content) {
return (
/*#__PURE__*/
// Ignored via go/ees005
// eslint-disable-next-line react/jsx-props-no-spreading
React.createElement(MarkComponent, _extends({
key: key
}, props), content)
);
}
getTableChildrenProps(node) {
return {
...this.getProps(node),
allowColumnSorting: this.allowColumnSorting
};
}
getTableProps(node, path = []) {
const isInsideOfBlockNode = insideBlockNode(path, node.type.schema);
const isInsideMultiBodiedExtension = insideMultiBodiedExtension(path, node.type.schema);
const isInsideOfTable = insideTable(path, node.type.schema);
const isStickySafeCenteringEnabled = expValEquals('platform_editor_flex_based_centering', 'isEnabled', true);
const isInsideBreakoutExpand = !isStickySafeCenteringEnabled && expValEquals('platform_editor_table_sticky_header_improvements', 'cohort', 'test_with_overflow') && expValEquals('platform_editor_table_sticky_header_patch_11', 'isEnabled', true) && insideBreakoutExpand(path);
const stickyHeaders = isStickySafeCenteringEnabled ? !isInsideOfTable ? this.stickyHeaders : undefined : !isInsideOfTable && !insideBreakoutLayout(path) && !isInsideBreakoutExpand ? this.stickyHeaders : undefined;
return {
...this.getProps(node),
allowColumnSorting: this.allowColumnSorting,
columnWidths: getColumnWidths(node),
tableNode: node,
stickyHeaders,
isInsideOfBlockNode,
isInsideOfTable,
isInsideMultiBodiedExtension,
allowTableAlignment: this.allowTableAlignment,
allowTableResizing: this.allowTableResizing,
// eslint-disable-next-line @atlaskit/platform/valid-gate-name
isPresentational: fg('platform_renderer_isPresentational') ? this.isPresentational : false,
disableTableOverflowShadow: this.disableTableOverflowShadow,
allowFixedColumnWidthOption: this.allowFixedColumnWidthOption
};
}
getDateProps(node, parentInfo, path = []) {
return {
timestamp: node.attrs && node.attrs.timestamp,
parentIsIncompleteTask: parentInfo && parentInfo.parentIsIncompleteTask,
dataAttributes: {
// We need to account for depth (path.length gives up depth) here
// but depth doesnt increment the pos, only accounted for.
'data-renderer-start-pos': this.startPos + path.length
}
};
}
getMediaSingleProps(node, path = []) {
const {
marks: {
link
}
} = node.type.schema;
const isInsideOfBlockNode = insideBlockNode(path, node.type.schema);
const isLinkMark = mark => mark.type === link;
const childHasLink = node.firstChild && node.firstChild.marks.filter(m => isLinkMark(m) || this.allowMediaLinking === true).length;
return {
...this.getProps(node, path),
isInsideOfBlockNode,
childHasLink,
allowCaptions: this.media && this.media.allowCaptions,
featureFlags: this.media && this.media.featureFlags
};
}
getMediaProps(node, path = []) {
var _this$media, _this$media2, _this$media3;
const {
marks: {
annotation,
link,
border
}
} = node.type.schema;
const isChildOfMediaSingle = path.some(n => {
var _n$type2;
return ((_n$type2 = n.type) === null || _n$type2 === void 0 ? void 0 : _n$type2.name) === 'mediaSingle';
});
// Only check path for bodiedSyncBlock; syncBlock uses RendererContext
const nestedUnder = editorExperiment('platform_synced_block', true) ? getNestedUnderNodes(path, ['bodiedSyncBlock']) : undefined;
const isAnnotationMark = mark => mark.type === annotation;
const isLinkMark = mark => mark.type === link;
const isBorderMark = mark => isChildOfMediaSingle && mark.type === border;
return {
...this.getProps(node, path),
marks: node.marks.filter(m => !isLinkMark(m) || this.allowMediaLinking === true),
isLinkMark,
isBorderMark,
isAnnotationMark,
allowAltTextOnImages: this.allowAltTextOnImages,
featureFlags: this.media && this.media.featureFlags,
shouldOpenMediaViewer: this.shouldOpenMediaViewer,
ssr: (_this$media = this.media) === null || _this$media === void 0 ? void 0 : _this$media.ssr,
// surroundTextNodesWithTextWrapper checks inlineComment.allowDraftMode
allowAnnotationsDraftMode: this.surroundTextNodesWithTextWrapper,
enableSyncMediaCard: (_this$media2 = this.media) === null || _this$media2 === void 0 ? void 0 : _this$media2.enableSyncMediaCard,
mediaViewerExtensions: (_this$media3 = this.media) === null || _this$media3 === void 0 ? void 0 : _this$media3.mediaViewerExtensions,
nestedUnder
};
}
getExtensionProps(node, path = []) {
var _this$getExtensionHei;
return {
...this.getProps(node, path),
extensionViewportSizes: this.extensionViewportSizes,
nodeHeight: (_this$getExtensionHei = this.getExtensionHeight) === null || _this$getExtensionHei === void 0 ? void 0 : _this$getExtensionHei.call(this, node),
shouldDisplayExtensionAsInline: this.shouldDisplayExtensionAsInline
};
}
getEmojiProps(node, path = []) {
return {
...this.getProps(node, path),
resourceConfig: this.emojiResourceConfig
};
}
getEmbedCardProps(node, path = []) {
const isInsideOfBlockNode = insideBlockNode(path, node.type.schema);
return {
...this.getProps(node),
isInsideOfBlockNode,
onSetLinkTarget: this.onSetLinkTarget
};
}
getBlockCardProps(node, path = []) {
return {
...this.getProps(node),
isNodeNested: path.length > 0,
onSetLinkTarget: this.onSetLinkTarget
};
}
getInlineCardProps(node, path = []) {
return {
...this.getProps(node, path),
onSetLinkTarget: this.onSetLinkTarget
};
}
getMediaGroupProps(node) {
var _this$media4, _this$media5;
return {
...this.getProps(node),
shouldOpenMediaViewer: this.shouldOpenMediaViewer,
allowAltTextOnImages: this.allowAltTextOnImages,
featureFlags: this.media && this.media.featureFlags,
enableDownloadButton: (_this$media4 = this.media) === null || _this$media4 === void 0 ? void 0 : _this$media4.enableDownloadButton,
ssr: (_this$media5 = this.media) === null || _this$media5 === void 0 ? void 0 : _this$media5.ssr
};
}
getMediaInlineProps(node) {
var _this$media6, _this$media7;
return {
...this.getProps(node),
ssr: (_this$media6 = this.media) === null || _this$media6 === void 0 ? void 0 : _this$media6.ssr,
fallbackMediaNameFetcher: (_this$media7 = this.media) === null || _this$media7 === void 0 ? void 0 : _this$media7.fallbackMediaNameFetcher
};
}
getTaskItemProps(node, path = []) {
return {
...this.getProps(node, path),
disabled: this.disableActions
};
}
getHardBreakProps(node, path = []) {
let forceNewLine = false;
const parentNode = path.length > 0 ? path[path.length - 1] : null;
if (parentNode && parentNode.lastChild === node) {
forceNewLine = true;
}
return {
...this.getProps(node),
forceNewLine
};
}
getCodeBlockProps(node) {
// The appearance being mobile indicates we are in an renderer being
// rendered by mobile bridge in a web view.
// The tooltip is likely to have unexpected behaviour there, with being cut
// off, so we disable it. This is also to keep the behaviour consistent with
// the rendering in the mobile Native Renderer.
const codeBidiWarningTooltipEnabled = false;
return {
...this.getProps(node),
text: node.textContent,
codeBidiWarningTooltipEnabled
};
}
getPanelProps(node) {
return {
...this.getProps(node),
allowCustomPanels: this.allowCustomPanels
};
}
getProps(node, path = []) {
const startPos = this.startPos + path.length;
return {
asInline: this.inlinePositions.has(startPos) ? 'on' : undefined,
text: node.text,
providers: this.providers,
eventHandlers: this.eventHandlers,
extensionHandlers: this.extensionHandlers,
portal: this.portal,
rendererContext: this.rendererContext,
serializer: this,
content: node.content ? node.content.toJSON() : undefined,
allowHeadingAnchorLinks: this.allowHeadingAnchorLinks,
allowCopyToClipboard: this.allowCopyToClipboard,
allowWrapCodeBlock: this.allowWrapCodeBlock,
allowPlaceholderText: this.allowPlaceholderText,
rendererAppearance: this.appearance,
rendererContentMode: this.contentMode,
fireAnalyticsEvent: this.fireAnalyticsEvent,
nodeType: node.type.name,
marks: node.marks,
smartLinks: this.smartLinks,
isInsideOfInlineExtension: this.isInsideOfInlineExtension,
dataAttributes: {
// We need to account for depth (path.length gives up depth) here
// but depth doesnt increment the pos, only accounted for.
'data-renderer-start-pos': startPos
},
startPos,
path,
...node.attrs
};
}
headingAnchorSupported(path = []) {
const isImmediateParent = (path, nodeName) => {
return path.length > 0 && path[path.length - 1].type.name === nodeName;
};
return isNestedHeaderLinksEnabled(this.allowHeadingAnchorLinks) || path.length === 0 || isImmediateParent(path, 'layoutColumn');
}
getHeadingProps(node, path = []) {
return {
...this.getProps(node, path),
content: node.content ? node.content.toJSON() : undefined,
headingId: this.getHeadingId(node, this.headingIds),
showAnchorLink: this.appearance !== 'comment' && this.allowHeadingAnchorLinks && !this.disableHeadingIDs && this.headingAnchorSupported(path)
};
}
getExpandProps(node, _path = []) {
let loadBodyContent = false;
if (fg('hot-121622_lazy_load_expand_content')) {
const annotations = findChildrenByMark(node, node.type.schema.marks.annotation, true);
// Force rendering children if there are inline comments to support comments navigation
// which relies on the HTML node to be present.
loadBodyContent = annotations.some(annotation => {
return annotation.node.marks.some(mark => mark.attrs.annotationType === 'inlineComment');
});
}
if (!isNestedHeaderLinksEnabled(this.allowHeadingAnchorLinks)) {
return {
...this.getProps(node),
loadBodyContent
};
}
const nestedHeaderIds = findChildrenByType(node, node.type.schema.nodes.heading).map(({
node
}) => this.getHeadingId(node, this.expandHeadingIds));
return {
...this.getProps(node),
nestedHeaderIds,
loadBodyContent
};
}
// The return value of this function is NOT url encoded,
// In HTML5 standard, id can contain any characters, encoding is no necessary.
// Plus we trying to avoid double encoding, therefore we leave the value as is.
// Remember to use encodeURIComponent when generating url from the id value.
getHeadingId(node, headingIds) {
if (this.disableHeadingIDs || !node.content.size) {
return;
}
// We are not use node.textContent here, because we would like to handle cases where
// headings only contain inline blocks like emoji, status and date.
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const nodeContent = node.content.toJSON()
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
.reduce((acc, node) => acc.concat(getText(node) || ''), '').trim()
// Ignored via go/ees005
// eslint-disable-next-line require-unicode-regexp
.replace(/\s/g, '-');
if (!nodeContent) {
return;
}
return this.getUniqueHeadingId(nodeContent, headingIds);
}
getUniqueHeadingId(baseId, headingIds, counter = 0) {
if (counter === 0 && headingIds.indexOf(baseId) === -1) {
headingIds.push(baseId);
return baseId;
} else if (counter !== 0) {
const headingId = `${baseId}.${counter}`;
if (headingIds.indexOf(headingId) === -1) {
headingIds.push(headingId);
return headingId;
}
}
return this.getUniqueHeadingId(baseId, headingIds, ++counter);
}
getChildNodes(fragment) {
let children = [];
fragment.forEach(node => {
children.push(node);
});
children = mergeTextNodes(children);
if (!!this.shouldDisplayExtensionAsInline && expValEquals('platform_editor_render_bodied_extension_as_inline', 'isEnabled', true)) {
markBlockAsInline({
nodes: children,
onMark: ({
pos
}) => {
this.inlinePositions.add(pos);
},
parentPos: this.startPos,
shouldDisplayExtensionAsInline: this.shouldDisplayExtensionAsInline
});
}
return children;
}
static getMarks(node) {
if (!node.marks || node.marks.length === 0) {
return [];
}
return getMarksByOrder(node.marks);
}
static buildMarkStructure(content) {
return mergeMarks(content.map(node => {
const nodeMarks = this.getMarks(node);
if (nodeMarks.length === 0) {
return node;
}
return nodeMarks.reverse().reduce((acc, mark) => {
const {
eq
} = mark;
return {
...mark,
eq,
content: [acc]
};
// Ignored via go/ees005
// eslint-disable-next-line @typescript-eslint/no-explicit-any
}, node);
}));
}
}