All files / roosterjs-content-model-plugins/lib/paste/WordDesktop removeListParagraphMargins.ts

100% Statements 18/18
100% Branches 6/6
100% Functions 5/5
100% Lines 16/16

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              1x                                     129x     638x   638x                                             1x   46x 382x 696x   382x 253x     129x 379x     129x   4x         125x 125x              
import type { CssRule } from 'roosterjs-content-model-types';
 
/**
 * CSS class selectors used by Word Desktop to mark list paragraph elements.
 * Word emits global CSS rules that apply margins to these classes, which we want
 * to suppress so that RoosterJS list indentation logic is used instead.
 */
const WORD_LIST_PARAGRAPH_SELECTORS = new Set([
    'p.MsoListParagraph',
    'p.MsoListParagraphCxSpFirst',
    'p.MsoListParagraphCxSpMiddle',
    'p.MsoListParagraphCxSpLast',
    'div.MsoListParagraph',
    'div.MsoListParagraphCxSpFirst',
    'div.MsoListParagraphCxSpMiddle',
    'div.MsoListParagraphCxSpLast',
]);
 
/**
 * @internal
 * Strips all margin-* properties from a CSS property string.
 * Empty tokens produced by a trailing semicolon are preserved so that the
 * resulting string still ends with ";" and remains safe to concatenate.
 * For example, "margin-top: 0pt; color: red;" becomes " color: red;".
 */
function removeMarginProperties(cssText: string): string {
    return cssText
        .split(';')
        .filter(prop => {
            const name = prop.split(':')[0].trim().toLowerCase();
            // Keep empty tokens (the trailing ';' produces one) and any non-margin property.
            return !name || !/^margin/.test(name);
        })
        .join(';');
}
 
/**
 * @internal
 * Removes margin properties from global CSS rules that target Word list paragraph
 * classes (p.MsoListParagraph, p.MsoListParagraphCxSpFirst, etc.).
 *
 * Word Desktop pastes a global stylesheet that typically includes rules like:
 *   p.MsoListParagraph { margin: 0in; margin-bottom: .0001pt; ... }
 * These margins conflict with RoosterJS's own list indentation, causing double
 * indentation when the CSS is converted to inline styles via convertInlineCss.
 *
 * When a rule's selectors are exclusively list paragraph classes the margins are
 * removed in place.  When a rule groups list paragraph classes with other selectors
 * the rule is split: the non-list selectors keep the original text, and a new rule
 * is inserted for the list paragraph selectors with margins stripped.
 *
 * The array is mutated in place so the changes are reflected when convertInlineCss
 * subsequently processes the same array reference.
 */
export function removeListParagraphMargins(globalCssRules: CssRule[]): void {
    // Iterate in reverse so that splice insertions don't shift unvisited indices.
    for (let i = globalCssRules.length - 1; i >= 0; i--) {
        const rule = globalCssRules[i];
        const matchingSelectors = rule.selectors.filter(s => WORD_LIST_PARAGRAPH_SELECTORS.has(s));
 
        if (matchingSelectors.length === 0) {
            continue;
        }
 
        const nonMatchingSelectors = rule.selectors.filter(
            s => !WORD_LIST_PARAGRAPH_SELECTORS.has(s)
        );
 
        if (nonMatchingSelectors.length === 0) {
            // All selectors target list paragraphs — strip margins directly.
            rule.text = removeMarginProperties(rule.text);
        } else {
            // Mixed rule: keep the non-list selectors on the original entry, then
            // insert a new entry immediately after for the list paragraph selectors
            // with margins removed.
            rule.selectors = nonMatchingSelectors;
            globalCssRules.splice(i + 1, 0, {
                selectors: matchingSelectors,
                text: removeMarginProperties(rule.text),
            });
        }
    }
}