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           68x     17x 15x       17x 17x                                           17x                     1x                                         17x   17x 3x     14x   14x 5x 5x                 1x 11x 11x 3x 3x 3x   11x 5x 5x 5x     11x               1x         17x 62x         8x 8x 8x 8x       17x             1x 91x 91x         53x     91x   91x 54x 54x      
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;
    }
};