All files / roosterjs-content-model-core/lib/corePlugin/format applyPendingFormat.ts

100% Statements 39/39
81.58% Branches 31/38
100% Functions 7/7
100% Lines 39/39

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 1211x                               1x 1x               1x           8x   8x   7x 7x         6x 6x 6x   6x 6x 6x     6x 5x       3x 3x           3x 3x   3x               3x 3x     3x     5x       2x   2x 2x         7x     7x 4x 4x     7x                 5x 5x   5x 7x 7x       5x    
import {
    createText,
    getObjectKeys,
    iterateSelections,
    mutateBlock,
    mutateSegment,
    normalizeContentModel,
    setParagraphNotImplicit,
} from 'roosterjs-content-model-dom';
import type {
    ContentModelBlockFormat,
    ContentModelFormatBase,
    ContentModelSegmentFormat,
    IEditor,
} from 'roosterjs-content-model-types';
 
const ANSI_SPACE = '\u0020';
const NON_BREAK_SPACE = '\u00A0';
 
/**
 * @internal
 * Apply pending format to the text user just input
 * @param editor The editor to get format from
 * @param data The text user just input
 */
export function applyPendingFormat(
    editor: IEditor,
    data: string,
    segmentFormat?: ContentModelSegmentFormat,
    paragraphFormat?: ContentModelBlockFormat
) {
    let isChanged = false;
 
    editor.formatContentModel(
        (model, context) => {
            iterateSelections(model, (_, __, block, segments) => {
                if (
                    block?.blockType == 'Paragraph' &&
                    segments?.length == 1 &&
                    segments[0].segmentType == 'SelectionMarker'
                ) {
                    const marker = segments[0];
                    const index = block.segments.indexOf(marker);
                    const previousSegment = block.segments[index - 1];
 
                    Eif (previousSegment?.segmentType == 'Text') {
                        const text = previousSegment.text;
                        const subStr = text.substr(-data.length, data.length);
 
                        // For space, there can be &#32 (space) or &#160 ( ), we treat them as the same
                        if (subStr == data || (data == ANSI_SPACE && subStr == NON_BREAK_SPACE)) {
                            if (
                                segmentFormat &&
                                !isSubFormatIncluded(previousSegment.format, segmentFormat)
                            ) {
                                mutateSegment(block, previousSegment, previousSegment => {
                                    previousSegment.text = text.substring(
                                        0,
                                        text.length - data.length
                                    );
                                });
 
                                mutateSegment(block, marker, (marker, block) => {
                                    marker.format = { ...segmentFormat };
 
                                    const newText = createText(
                                        data == ANSI_SPACE ? NON_BREAK_SPACE : data,
                                        {
                                            ...previousSegment.format,
                                            ...segmentFormat,
                                        }
                                    );
 
                                    block.segments.splice(index, 0, newText);
                                    setParagraphNotImplicit(block);
                                });
 
                                isChanged = true;
                            }
 
                            if (
                                paragraphFormat &&
                                !isSubFormatIncluded(block.format, paragraphFormat)
                            ) {
                                const mutableParagraph = mutateBlock(block);
 
                                Object.assign(mutableParagraph.format, paragraphFormat);
                                isChanged = true;
                            }
                        }
                    }
                }
                return true;
            });
 
            if (isChanged) {
                normalizeContentModel(model);
                context.skipUndoSnapshot = true;
            }
 
            return isChanged;
        },
        {
            apiName: 'applyPendingFormat',
        }
    );
}
 
function isSubFormatIncluded<T extends ContentModelFormatBase>(containerFormat: T, subFormat: T) {
    const keys = getObjectKeys(subFormat);
    let result = true;
 
    keys.forEach(key => {
        Eif (containerFormat[key] !== subFormat[key]) {
            result = false;
        }
    });
 
    return result;
}