All files / roosterjs-content-model-plugins/lib/paste/Excel processPastedContentFromExcel.ts

91.23% Statements 52/57
73.33% Branches 44/60
85.71% Functions 6/7
91.07% Lines 51/56

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 166 167 168 169 170 171 172 173 174 175 176 1771x 1x 1x               1x 1x 1x 1x 1x 1x                   1x           64x     16x 14x       16x 16x                                           16x                     1x                                         16x   16x 2x     14x   14x 5x 5x                 1x 11x 11x 3x 3x 3x   11x 5x 5x 5x     11x               1x         16x 38x         8x 8x 8x 8x       16x             1x 59x 59x         29x     59x   59x 30x 30x      
import { addParser } from '../utils/addParser';
import { isNodeOfType, moveChildNodes } from 'roosterjs-content-model-dom';
import { setProcessor } from '../utils/setProcessor';
import type {
    BeforePasteEvent,
    ClipboardData,
    DOMCreator,
    ElementProcessor,
} from 'roosterjs-content-model-types';
 
const LAST_TD_END_REGEX = /<\/\s*td\s*>((?!<\/\s*tr\s*>)[\s\S])*$/i;
const LAST_TR_END_REGEX = /<\/\s*tr\s*>((?!<\/\s*table\s*>)[\s\S])*$/i;
const LAST_TR_REGEX = /<tr[^>]*>[^<]*/i;
const LAST_TABLE_REGEX = /<table[^>]*>[^<]*/i;
const TABLE_SELECTOR = 'table';
const DEFAULT_BORDER_STYLE = 'solid 1px #d4d4d4';
 
/**
 * @internal
 * Convert pasted content from Excel, add borders when source doc doesn't have a border
 * @param event The BeforePaste event
 * @param domCreator The DOM creator
 * @param allowExcelNoBorderTable Allow table copied from Excel without border
 * @param isNativeEvent Whether the event is native event
 */
export function processPastedContentFromExcel(
    event: BeforePasteEvent,
    domCreator: DOMCreator,
    allowExcelNoBorderTable: boolean,
    isNativeEvent: boolean
) {
    const { fragment, htmlBefore, htmlAfter, clipboardData } = event;
 
    // For non native event we already validated that the content contains a table
    if (isNativeEvent) {
        validateExcelFragment(fragment, domCreator, htmlBefore, clipboardData, htmlAfter);
    }
 
    // For Excel Online
    const firstChild = fragment.firstChild;
    Iif (
        isNodeOfType(firstChild, 'ELEMENT_NODE') &&
        firstChild.tagName == 'div' &&
        firstChild.firstChild
    ) {
        const tableFound = Array.from(firstChild.childNodes).every((child: Node) => {
            // Tables pasted from Excel Online should be of the format: 0 to N META tags and 1 TABLE tag
            const tagName = isNodeOfType(child, 'ELEMENT_NODE') && child.tagName;
 
            return tagName == 'META'
                ? true
                : tagName == 'TABLE'
                ? child == firstChild.lastChild
                : false;
        });
 
        // Extract Table from Div
        if (tableFound && firstChild.lastChild) {
            event.fragment.replaceChildren(firstChild.lastChild);
        }
    }
 
    setupExcelTableHandlers(
        event,
        allowExcelNoBorderTable,
        isNativeEvent /* handleForNativeEvent */
    );
}
 
/**
 * @internal
 * Exported only for unit test
 */
export function validateExcelFragment(
    fragment: DocumentFragment,
    domCreator: DOMCreator,
    htmlBefore: string,
    clipboardData: ClipboardData,
    htmlAfter: string
) {
    // Clipboard content of Excel may contain the <StartFragment> and EndFragment comment tags inside the table
    //
    // @example
    // <table>
    // <!--StartFragment-->
    // <tr>...</tr>
    // <!--EndFragment-->
    // </table>
    //
    // This causes that the fragment is not properly created and the table is not extracted.
    // The content that is before the StartFragment is htmlBefore and the content that is after the EndFragment is htmlAfter.
    // So attempt to create a new document fragment with the content of htmlBefore + clipboardData.html + htmlAfter
    // If a table is found, replace the fragment with the new fragment
    const result =
        !fragment.querySelector(TABLE_SELECTOR) &&
        domCreator.htmlToDOM(htmlBefore + clipboardData.html + htmlAfter);
    if (result && result.querySelector(TABLE_SELECTOR)) {
        moveChildNodes(fragment, result?.body);
    } else {
        // If the table is still not found, try to extract the table from the clipboard data using Regex
        const html = clipboardData.html ? excelHandler(clipboardData.html, htmlBefore) : undefined;
 
        if (html && clipboardData.html != html) {
            const doc = domCreator.htmlToDOM(html);
            moveChildNodes(fragment, doc?.body);
        }
    }
}
 
/**
 * @internal Export for test only
 * @param html Source html
 */
export function excelHandler(html: string, htmlBefore: string): string {
    try {
        if (html.match(LAST_TD_END_REGEX)) {
            const trMatch = htmlBefore.match(LAST_TR_REGEX);
            const tr = trMatch ? trMatch[0] : '<TR>';
            html = tr + html + '</TR>';
        }
        if (html.match(LAST_TR_END_REGEX)) {
            const tableMatch = htmlBefore.match(LAST_TABLE_REGEX);
            const table = tableMatch ? tableMatch[0] : '<TABLE>';
            html = table + html + '</TABLE>';
        }
    } finally {
        return html;
    }
}
 
/**
 * @internal
 * Exported only for unit test
 */
export function setupExcelTableHandlers(
    event: BeforePasteEvent,
    allowExcelNoBorderTable: boolean | undefined,
    isNativeEvent: boolean
) {
    addParser(event.domToModelOption, 'tableCell', (format, element) => {
        if (
            !allowExcelNoBorderTable &&
            (element.style.borderStyle === 'none' ||
                (!isNativeEvent && element.style.borderStyle == ''))
        ) {
            format.borderBottom = DEFAULT_BORDER_STYLE;
            format.borderLeft = DEFAULT_BORDER_STYLE;
            format.borderRight = DEFAULT_BORDER_STYLE;
            format.borderTop = DEFAULT_BORDER_STYLE;
        }
    });
 
    setProcessor(event.domToModelOption, 'child', childProcessor);
}
 
/**
 * @internal
 * Exported only for unit test
 */
export const childProcessor: ElementProcessor<ParentNode> = (group, element, context) => {
    const segmentFormat = { ...context.segmentFormat };
    if (
        group.blockGroupType === 'TableCell' &&
        group.format.textColor &&
        !context.segmentFormat.textColor
    ) {
        context.segmentFormat.textColor = group.format.textColor;
    }
 
    context.defaultElementProcessors.child(group, element, context);
 
    if (group.blockGroupType === 'TableCell' && group.format.textColor) {
        context.segmentFormat = segmentFormat;
        delete group.format.textColor;
    }
};