All files / roosterjs-content-model-dom/lib/modelToDom/handlers handleParagraph.ts

100% Statements 67/67
97.22% Branches 70/72
100% Functions 6/6
100% Lines 60/60

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 1661x 1x 1x 1x 1x 1x                 1x         1x             1224x   1224x 40x   1184x   1184x     96x 1184x             1184x   1184x   1184x         1184x 1184x   1184x 1184x   1184x   1184x       115x                         1184x 1493x   1493x       1493x 1493x               1493x 1315x       1184x       1184x 831x   831x 831x 831x             831x   353x     1184x               1184x   1184x 1184x 1184x     1184x 831x 670x     831x   353x 353x         1224x       316x 327x 327x 46x   281x 132x   149x     35x    
import { applyFormat } from '../utils/applyFormat';
import { getObjectKeys } from '../../domUtils/getObjectKeys';
import { optimize } from '../optimizers/optimize';
import { reuseCachedElement } from '../../domUtils/reuseCachedElement';
import { stackFormat } from '../utils/stackFormat';
import { unwrap } from '../../domUtils/unwrap';
import type {
    ContentModelBlockHandler,
    ContentModelParagraph,
    ContentModelSegment,
    ModelToDomContext,
    ModelToDomSegmentContext,
} from 'roosterjs-content-model-types';
 
const DefaultParagraphTag = 'div';
 
/**
 * @internal
 */
export const handleParagraph: ContentModelBlockHandler<ContentModelParagraph> = (
    doc: Document,
    parent: Node,
    paragraph: ContentModelParagraph,
    context: ModelToDomContext,
    refNode: Node | null
) => {
    let container = context.allowCacheElement ? paragraph.cachedElement : undefined;
 
    if (container && paragraph.segments.every(x => x.segmentType != 'General' && !x.isSelected)) {
        refNode = reuseCachedElement(parent, container, refNode, context.rewriteFromModel);
    } else {
        stackFormat(context, paragraph.decorator?.tagName || null, () => {
            const needParagraphWrapper =
                !paragraph.isImplicit ||
                !!paragraph.decorator ||
                (getObjectKeys(paragraph.format).length > 0 &&
                    paragraph.segments.some(segment => segment.segmentType != 'SelectionMarker'));
            const formatOnWrapper = needParagraphWrapper
                ? {
                      ...(paragraph.decorator?.format || {}),
                      ...paragraph.segmentFormat,
                  }
                : {};
 
            container = doc.createElement(paragraph.decorator?.tagName || DefaultParagraphTag);
 
            parent.insertBefore(container, refNode);
 
            context.regularSelection.current = {
                block: needParagraphWrapper ? container : container.parentNode,
                segment: null,
            };
 
            const handleSegments = () => {
                const parent = container;
 
                Eif (parent) {
                    const firstSegment = paragraph.segments[0];
 
                    const segmentContext: ModelToDomSegmentContext = context;
 
                    if (firstSegment?.segmentType == 'SelectionMarker') {
                        // Make sure there is a segment created before selection marker.
                        // If selection marker is the first selected segment in a paragraph, create a dummy text node,
                        // so after rewrite, the regularSelection object can have a valid segment object set to the text node.
                        context.modelHandlers.text(
                            doc,
                            parent,
                            {
                                ...firstSegment,
                                segmentType: 'Text',
                                text: '',
                            },
                            segmentContext,
                            []
                        );
                    }
 
                    for (let i = 0; i < paragraph.segments.length; i++) {
                        const segment = paragraph.segments[i];
 
                        segmentContext.noFollowingTextSegmentOrLast =
                            i === paragraph.segments.length - 1 ||
                            !hasTextSegmentAfter(paragraph.segments, i);
 
                        const newSegments: Node[] = [];
                        context.modelHandlers.segment(
                            doc,
                            parent,
                            segment,
                            segmentContext,
                            newSegments
                        );
 
                        for (const node of newSegments) {
                            context.domIndexer?.onSegment(node, paragraph, [segment]);
                        }
                    }
 
                    delete segmentContext.noFollowingTextSegmentOrLast;
                }
            };
 
            if (needParagraphWrapper) {
                stackFormat(context, formatOnWrapper, handleSegments);
 
                applyFormat(container, context.formatAppliers.block, paragraph.format, context);
                applyFormat(container, context.formatAppliers.container, paragraph.format, context);
                applyFormat(
                    container,
                    context.formatAppliers.segmentOnBlock,
                    formatOnWrapper,
                    context
                );
 
                context.paragraphMap?.applyMarkerToDom(container, paragraph);
            } else {
                handleSegments();
            }
 
            optimize(container, context);
 
            // It is possible the next sibling node is changed during processing child segments
            // e.g. When this paragraph is an implicit paragraph and it contains an inline entity segment
            // The segment will be appended to container as child then the container will be removed
            // since this paragraph it is implicit. In that case container.nextSibling will become original
            // inline entity's next sibling. So reset refNode to its real next sibling (after change) here
            // to make sure the value is correct.
            refNode = container.nextSibling;
 
            Eif (container) {
                context.onNodeCreated?.(paragraph, container);
                context.domIndexer?.onParagraph(container);
            }
 
            if (needParagraphWrapper) {
                if (context.allowCacheElement) {
                    paragraph.cachedElement = container;
                }
 
                context.rewriteFromModel.addedBlockElements.push(container);
            } else {
                unwrap(container);
                container = undefined;
            }
        });
    }
 
    return refNode;
};
 
function hasTextSegmentAfter(segments: ReadonlyArray<ContentModelSegment>, index: number): boolean {
    for (let i = index + 1; i < segments.length; i++) {
        const type = segments[i].segmentType;
        if (type === 'SelectionMarker') {
            continue;
        }
        if (type === 'Text') {
            return true;
        } else {
            return false;
        }
    }
    return false;
}