Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -1,24 +1,15 @@
import { addParser } from '../utils/addParser';
import { adjustPercentileLineHeight } from '../parsers/adjustPercentileLineHeightParser';
import { getStyleMetadata } from './getStyleMetadata';
import { getStyles } from '../utils/getStyles';
import { listLevelParser } from '../parsers/listLevelParser';
import { processWordComments } from './processWordComments';
import { processWordList } from './processWordLists';
import { removeNegativeTextIndentParser } from '../parsers/removeNegativeTextIndentParser';
import { setProcessor } from '../utils/setProcessor';
import { wordTableParser } from '../parsers/wordTableParser';
import type { WordMetadata } from './WordMetadata';
import type {
BeforePasteEvent,
ContentModelBlockFormat,
ContentModelListItemLevelFormat,
ContentModelTableFormat,
DomToModelContext,
ElementProcessor,
FormatParser,
} from 'roosterjs-content-model-types';

const PERCENTAGE_REGEX = /%/;
// Default line height in browsers according to https://developer.mozilla.org/en-US/docs/Web/CSS/line-height#normal
const DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE = 1.2;
import type { BeforePasteEvent, ElementProcessor } from 'roosterjs-content-model-types';

/**
* @internal
Expand Down Expand Up @@ -52,39 +43,3 @@ const wordDesktopElementProcessor = (
}
};
};

function adjustPercentileLineHeight(format: ContentModelBlockFormat, element: HTMLElement): void {
//If the line height is less than the browser default line height, line between the text is going to be too narrow
let parsedLineHeight: number;
if (
PERCENTAGE_REGEX.test(element.style.lineHeight) &&
!isNaN((parsedLineHeight = parseInt(element.style.lineHeight)))
) {
format.lineHeight = (
DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE *
(parsedLineHeight / 100)
).toString();
}
}

const listLevelParser: FormatParser<ContentModelListItemLevelFormat> = (
format: ContentModelListItemLevelFormat,
element: HTMLElement,
_context: DomToModelContext,
defaultStyle: Readonly<Partial<CSSStyleDeclaration>>
) => {
if (element.style.marginLeft != '') {
format.marginLeft = defaultStyle.marginLeft;
}

format.marginBottom = undefined;
};

const wordTableParser: FormatParser<ContentModelTableFormat> = (format, element): void => {
if (format.marginLeft?.startsWith('-')) {
delete format.marginLeft;
}
if (format.htmlAlign) {
delete format.htmlAlign;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import type { ContentModelBlockFormat } from 'roosterjs-content-model-types';

const PERCENTAGE_REGEX = /%/;
// Default line height in browsers according to https://developer.mozilla.org/en-US/docs/Web/CSS/line-height#normal
const DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE = 1.2;

/**
* @internal
* Parser for adjusting percentage-based line heights and converting 'normal' to a specific percentage
* @param format The block format to modify
* @param element The HTML element being processed
*/
export function adjustPercentileLineHeight(
format: ContentModelBlockFormat,
element: HTMLElement
): void {
// If the line height is less than the browser default line height, line between the text is going to be too narrow
let parsedLineHeight: number;
if (
PERCENTAGE_REGEX.test(element.style.lineHeight) &&
!isNaN((parsedLineHeight = parseInt(element.style.lineHeight)))
) {
format.lineHeight = (
DEFAULT_BROWSER_LINE_HEIGHT_PERCENTAGE *
(parsedLineHeight / 100)
).toString();
} else if (element.style.lineHeight.toLowerCase() === 'normal') {
format.lineHeight = '120%';
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import type {
ContentModelListItemLevelFormat,
DomToModelContext,
FormatParser,
} from 'roosterjs-content-model-types';

/**
* @internal
* Parser for processing list level formatting specific to Word Desktop
* @param format The list item level format to modify
* @param element The HTML element being processed
* @param _context The DOM to model context
* @param defaultStyle The default style properties
*/
export const listLevelParser: FormatParser<ContentModelListItemLevelFormat> = (
format: ContentModelListItemLevelFormat,
element: HTMLElement,
_context: DomToModelContext,
defaultStyle: Readonly<Partial<CSSStyleDeclaration>>
) => {
if (element.style.marginLeft !== '') {
format.marginLeft = defaultStyle.marginLeft;
}

format.marginBottom = undefined;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import type { ContentModelTableFormat, FormatParser } from 'roosterjs-content-model-types';

/**
* @internal
* Parser for processing table formatting specific to Word Desktop
* @param format The table format to modify
* @param element The HTML element being processed
*/
export const wordTableParser: FormatParser<ContentModelTableFormat> = (format, element): void => {
if (format.marginLeft?.startsWith('-')) {
delete format.marginLeft;
}
if (format.htmlAlign) {
delete format.htmlAlign;
}
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import { adjustPercentileLineHeight } from '../../../lib/paste/parsers/adjustPercentileLineHeightParser';
import { ContentModelBlockFormat } from 'roosterjs-content-model-types';

describe('adjustPercentileLineHeight', () => {
let format: ContentModelBlockFormat;
let element: HTMLElement;

beforeEach(() => {
format = {};
element = document.createElement('p');
});

it('should convert percentage line height using browser default multiplier', () => {
element.style.lineHeight = '100%';
adjustPercentileLineHeight(format, element);
expect(format.lineHeight).toBe('1.2');
});

it('should convert percentage line height with different values', () => {
element.style.lineHeight = '150%';
adjustPercentileLineHeight(format, element);
expect(parseFloat(format.lineHeight!)).toBeCloseTo(1.8, 5);
});

it('should convert percentage line height with low values', () => {
element.style.lineHeight = '50%';
adjustPercentileLineHeight(format, element);
expect(parseFloat(format.lineHeight!)).toBeCloseTo(0.6, 5);
});

it('should convert normal line height to 120%', () => {
element.style.lineHeight = 'normal';
adjustPercentileLineHeight(format, element);
expect(format.lineHeight).toBe('120%');
});

it('should convert NORMAL (uppercase) line height to 120% - case insensitive', () => {
element.style.lineHeight = 'NORMAL';
adjustPercentileLineHeight(format, element);
expect(format.lineHeight).toBe('120%');
});

it('should not modify line height when it is not percentage or normal', () => {
element.style.lineHeight = '1.5';
adjustPercentileLineHeight(format, element);
expect(format.lineHeight).toBeUndefined();
});

it('should not modify line height when percentage is invalid', () => {
element.style.lineHeight = 'abc%';
adjustPercentileLineHeight(format, element);
expect(format.lineHeight).toBeUndefined();
});

it('should not modify line height when no line height is set', () => {
adjustPercentileLineHeight(format, element);
expect(format.lineHeight).toBeUndefined();
});

it('should handle mixed case normal values', () => {
element.style.lineHeight = 'Normal';
adjustPercentileLineHeight(format, element);
expect(format.lineHeight).toBe('120%');
});

it('should not affect existing format properties', () => {
format.marginTop = '10px';
format.marginBottom = '5px';
element.style.lineHeight = 'normal';
adjustPercentileLineHeight(format, element);
expect(format.lineHeight).toBe('120%');
expect(format.marginTop).toBe('10px');
expect(format.marginBottom).toBe('5px');
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import { ContentModelListItemLevelFormat, DomToModelContext } from 'roosterjs-content-model-types';
import { listLevelParser } from '../../../lib/paste/parsers/listLevelParser';

describe('listLevelParser', () => {
let format: ContentModelListItemLevelFormat;
let element: HTMLElement;
let context: DomToModelContext;
let defaultStyle: Readonly<Partial<CSSStyleDeclaration>>;

beforeEach(() => {
format = {};
element = document.createElement('li');
context = {} as any;
defaultStyle = {};
});

it('should set marginLeft from defaultStyle when element has marginLeft', () => {
element.style.marginLeft = '20px';
defaultStyle = { marginLeft: '15px' };
listLevelParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBe('15px');
expect(format.marginBottom).toBeUndefined();
});

it('should not set marginLeft when element marginLeft is empty', () => {
element.style.marginLeft = '';
defaultStyle = { marginLeft: '15px' };
listLevelParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBeUndefined();
expect(format.marginBottom).toBeUndefined();
});

it('should always set marginBottom to undefined', () => {
format.marginBottom = '10px';
listLevelParser(format, element, context, defaultStyle);
expect(format.marginBottom).toBeUndefined();
});

it('should handle undefined defaultStyle marginLeft', () => {
element.style.marginLeft = '20px';
defaultStyle = {};
listLevelParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBeUndefined();
expect(format.marginBottom).toBeUndefined();
});

it('should preserve other format properties', () => {
format.marginRight = '10px';
format.marginTop = '5px';
element.style.marginLeft = '20px';
defaultStyle = { marginLeft: '15px' };
listLevelParser(format, element, context, defaultStyle);
expect(format.marginRight).toBe('10px');
expect(format.marginTop).toBe('5px');
expect(format.marginLeft).toBe('15px');
expect(format.marginBottom).toBeUndefined();
});

it('should handle element with no marginLeft style', () => {
defaultStyle = { marginLeft: '15px' };
listLevelParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBeUndefined();
expect(format.marginBottom).toBeUndefined();
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
import { ContentModelTableFormat, DomToModelContext } from 'roosterjs-content-model-types';
import { wordTableParser } from '../../../lib/paste/parsers/wordTableParser';

describe('wordTableParser', () => {
let format: ContentModelTableFormat;
let element: HTMLElement;
let context: DomToModelContext;
let defaultStyle: Readonly<Partial<CSSStyleDeclaration>>;

beforeEach(() => {
format = {};
element = document.createElement('table');
context = {} as any;
defaultStyle = {};
});

it('should remove marginLeft when it starts with negative value', () => {
format.marginLeft = '-5px';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBeUndefined();
});

it('should remove marginLeft when it starts with negative number', () => {
format.marginLeft = '-10.5px';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBeUndefined();
});

it('should not remove marginLeft when it is positive', () => {
format.marginLeft = '5px';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBe('5px');
});

it('should not remove marginLeft when it is zero', () => {
format.marginLeft = '0px';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBe('0px');
});

it('should remove htmlAlign property', () => {
format.htmlAlign = 'center';
wordTableParser(format, element, context, defaultStyle);
expect(format.htmlAlign).toBeUndefined();
});

it('should handle both negative marginLeft and htmlAlign together', () => {
format.marginLeft = '-8px';
format.htmlAlign = 'start';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBeUndefined();
expect(format.htmlAlign).toBeUndefined();
});

it('should preserve other format properties', () => {
format.marginTop = '10px';
format.marginRight = '5px';
format.marginLeft = '-3px';
format.htmlAlign = 'end';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginTop).toBe('10px');
expect(format.marginRight).toBe('5px');
expect(format.marginLeft).toBeUndefined();
expect(format.htmlAlign).toBeUndefined();
});

it('should handle undefined marginLeft', () => {
format.htmlAlign = 'center';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBeUndefined();
expect(format.htmlAlign).toBeUndefined();
});

it('should handle empty marginLeft string', () => {
format.marginLeft = '';
format.htmlAlign = 'center';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBe('');
expect(format.htmlAlign).toBeUndefined();
});

it('should not affect format when no negative marginLeft or htmlAlign', () => {
format.marginLeft = '5px';
format.marginTop = '10px';
wordTableParser(format, element, context, defaultStyle);
expect(format.marginLeft).toBe('5px');
expect(format.marginTop).toBe('10px');
});
});
Loading
Loading