UNPKG

@atlaskit/renderer

Version:
739 lines (734 loc) • 29.6 kB
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); })); } }