All files / roosterjs-content-model-api/lib/modelApi/block toggleModelBlockQuote.ts

100% Statements 28/28
100% Branches 20/20
100% Functions 10/10
100% Lines 23/23

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 951x 1x 1x                                               1x         15x   15x     9x     15x   4x 3x     11x 11x 11x 11x         18x   18x 18x     17x       11x     15x             18x       50x                             15x    
import { splitSelectedParagraphByBr } from './splitSelectedParagraphByBr';
import { wrapBlockStep1, wrapBlockStep2 } from '../common/wrapBlock';
import {
    getOperationalBlocks,
    isBlockGroupOfType,
    areSameFormats,
    createFormatContainer,
    unwrapBlock,
} from 'roosterjs-content-model-dom';
import type { WrapBlockStep1Result } from '../common/wrapBlock';
import type {
    ContentModelBlockGroup,
    ContentModelFormatContainer,
    ContentModelFormatContainerFormat,
    ContentModelListItem,
    ReadonlyContentModelBlock,
    ReadonlyContentModelDocument,
    ReadonlyContentModelFormatContainer,
    ReadonlyContentModelListItem,
    ReadonlyOperationalBlocks,
    ShallowMutableContentModelBlock,
} from 'roosterjs-content-model-types';
 
/**
 * @internal
 */
export function toggleModelBlockQuote(
    model: ReadonlyContentModelDocument,
    formatLtr: ContentModelFormatContainerFormat,
    formatRtl: ContentModelFormatContainerFormat
): boolean {
    splitSelectedParagraphByBr(model);
 
    const paragraphOfQuote = getOperationalBlocks<
        ContentModelFormatContainer | ContentModelListItem
    >(model, ['FormatContainer', 'ListItem'], ['TableCell'], true /*deepFirst*/, block => {
        return block.blockGroupType == 'FormatContainer' ? block.tagName == 'blockquote' : true;
    });
 
    if (areAllBlockQuotes(paragraphOfQuote)) {
        // All selections are already in quote, we need to unquote them
        paragraphOfQuote.forEach(({ block, parent }) => {
            unwrapBlock(parent, block);
        });
    } else {
        const step1Results: WrapBlockStep1Result<ContentModelFormatContainer>[] = [];
        const creator = (isRtl: boolean) =>
            createFormatContainer('blockquote', isRtl ? formatRtl : formatLtr);
        const canMerge = (
            isRtl: boolean,
            target: ShallowMutableContentModelBlock,
            current?: ContentModelFormatContainer
        ): target is ContentModelFormatContainer =>
            canMergeQuote(target, current?.format || (isRtl ? formatRtl : formatLtr));
 
        paragraphOfQuote.forEach(({ block, parent }) => {
            if (isQuote(block)) {
                // Already in quote, no op
            } else {
                wrapBlockStep1(step1Results, parent, block, creator, canMerge);
            }
        });
 
        wrapBlockStep2(step1Results, canMerge);
    }
 
    return paragraphOfQuote.length > 0;
}
 
function canMergeQuote(
    target: ShallowMutableContentModelBlock,
    format: ContentModelFormatContainerFormat
): target is ContentModelFormatContainer {
    return isQuote(target) && areSameFormats(format, target.format);
}
 
function isQuote(block: ReadonlyContentModelBlock): block is ReadonlyContentModelFormatContainer {
    return (
        isBlockGroupOfType<ContentModelFormatContainer>(block, 'FormatContainer') &&
        block.tagName == 'blockquote'
    );
}
 
function areAllBlockQuotes(
    blockAndParents: ReadonlyOperationalBlocks<
        ReadonlyContentModelFormatContainer | ReadonlyContentModelListItem
    >[]
): blockAndParents is {
    block: ContentModelFormatContainer;
    parent: ContentModelBlockGroup;
    path: ContentModelBlockGroup[];
}[] {
    return blockAndParents.every(blockAndParent => isQuote(blockAndParent.block));
}