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
Expand Up @@ -85,8 +85,14 @@ export function processWordList(
setupListFormat(listType, element, context, wordLevel, listFormat, group, [
wordListPaddingParser,
]);
(listFormat.levels[listFormat.levels.length - 1]
.format as WordListFormat).wordList = wordList;
const currentLevelFormat = listFormat.levels[listFormat.levels.length - 1]
.format as WordListFormat;
currentLevelFormat.wordList = wordList;

if (wordLevel > 1) {
delete currentLevelFormat.paddingLeft;
delete currentLevelFormat.paddingRight;
}

const bullet = getBulletFromMetadata(listMetadata, listType);
const listFormatMetadata = bullet
Expand All @@ -103,6 +109,11 @@ export function processWordList(
listFormatMetadata,
getBulletElement(element),
listItem => {
if (wordLevel > 1) {
delete listItem.format.marginLeft;
delete listItem.format.marginRight;
}

if (listType == 'OL') {
setStartNumber(listItem, context, listMetadata, element);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,36 +18,117 @@ const WORD_LIST_PARAGRAPH_SELECTORS = new Set([

/**
* @internal
* Strips all margin-* properties from a CSS property string.
* Strips horizontal margin properties from a CSS property string while preserving
* vertical values from the margin shorthand.
* 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;".
* For example, "margin: 1pt 2pt 3pt 4pt; color: red;" becomes
* "margin-top: 1pt; margin-bottom: 3pt; 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(';');
function removeHorizontalMarginProperties(cssText: string): string {
const result: string[] = [];

cssText.split(';').forEach(prop => {
const separatorIndex = prop.indexOf(':');
const name = prop
.substring(0, separatorIndex < 0 ? prop.length : separatorIndex)
.trim()
.toLowerCase();

if (
name == 'margin-left' ||
name == 'margin-right' ||
name == 'margin-inline' ||
name == 'margin-inline-start' ||
name == 'margin-inline-end'
) {
return;
}

if (name == 'margin' && separatorIndex >= 0) {
const value = prop.substring(separatorIndex + 1).trim();
const importantMatch = value.match(/\s*!important\s*$/i);
const important = importantMatch ? ' !important' : '';
const values = splitCssValue(value.substring(0, importantMatch?.index ?? value.length));

if (values.length >= 1 && values.length <= 4) {
const leadingWhitespace = prop.match(/^\s*/)?.[0] || '';
const top = values[0];
const bottom = values.length >= 3 ? values[2] : values[0];

result.push(`${leadingWhitespace}margin-top: ${top}${important}`);
result.push(` margin-bottom: ${bottom}${important}`);
return;
}
}

result.push(prop);
});

return result.join(';');
}

function splitCssValue(value: string): string[] {
const result: string[] = [];
let current = '';
let quote: string | null = null;
let parenthesisDepth = 0;

for (let i = 0; i < value.length; i++) {
const char = value[i];

if (quote) {
current += char;

if (char == '\\') {
i++;
current += value[i] || '';
} else if (char == quote) {
quote = null;
}
} else if (char == '"' || char == "'") {
current += char;
quote = char;
} else if (char == '(') {
current += char;
parenthesisDepth++;
} else if (char == ')') {
current += char;
parenthesisDepth = Math.max(0, parenthesisDepth - 1);
} else if (parenthesisDepth == 0 && /\s/.test(char)) {
if (current) {
result.push(current);
current = '';
}
} else {
current += char;
}
}

if (current) {
result.push(current);
}

return result;
}

/**
* @internal
* Removes margin properties from global CSS rules that target Word list paragraph
* classes (p.MsoListParagraph, p.MsoListParagraphCxSpFirst, etc.).
* Removes horizontal 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.
* Horizontal margins conflict with RoosterJS's own list indentation, causing double
* indentation when the CSS is converted to inline styles via convertInlineCss. The
* vertical margins are preserved so Word's margin-top: 0 is not replaced by the
* browser's default paragraph margin when the list level is created.
*
* 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.
* When a rule's selectors are exclusively list paragraph classes the horizontal
* 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 horizontal margins
* stripped.
*
* The array is mutated in place so the changes are reflected when convertInlineCss
* subsequently processes the same array reference.
Expand All @@ -67,16 +148,15 @@ export function removeListParagraphMargins(globalCssRules: CssRule[]): void {
);

if (nonMatchingSelectors.length === 0) {
// All selectors target list paragraphs — strip margins directly.
rule.text = removeMarginProperties(rule.text);
// All selectors target list paragraphs — strip horizontal margins directly.
rule.text = removeHorizontalMarginProperties(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.
// insert a new entry immediately after for the list paragraph selectors.
rule.selectors = nonMatchingSelectors;
globalCssRules.splice(i + 1, 0, {
selectors: matchingSelectors,
text: removeMarginProperties(rule.text),
text: removeHorizontalMarginProperties(rule.text),
});
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -863,14 +863,14 @@ describe(ID, () => {
listType: 'OL',
format: {
startNumberOverride: 1,
marginTop: '1em',
marginTop: '0cm',
listStyleType: 'decimal',
},
dataset: { editingInfo: '{"orderedStyleType":1}' },
},
],
blockType: 'BlockGroup',
format: { lineHeight: '115%' },
format: { marginTop: '0cm', marginBottom: '0cm', lineHeight: '115%' },
blockGroupType: 'ListItem',
blocks: [
{
Expand Down Expand Up @@ -909,12 +909,12 @@ describe(ID, () => {
levels: [
{
listType: 'OL',
format: { marginTop: '1em', listStyleType: 'decimal' },
format: { marginTop: '0cm', listStyleType: 'decimal' },
dataset: { editingInfo: '{"orderedStyleType":1}' },
},
],
blockType: 'BlockGroup',
format: { lineHeight: '115%' },
format: { marginTop: '0cm', marginBottom: '0cm', lineHeight: '115%' },
blockGroupType: 'ListItem',
blocks: [
{
Expand Down Expand Up @@ -953,12 +953,12 @@ describe(ID, () => {
levels: [
{
listType: 'OL',
format: { marginTop: '1em', listStyleType: 'decimal' },
format: { marginTop: '0cm', listStyleType: 'decimal' },
dataset: { editingInfo: '{"orderedStyleType":1}' },
},
],
blockType: 'BlockGroup',
format: { lineHeight: '115%' },
format: { marginTop: '0cm', marginBottom: '0cm', lineHeight: '115%' },
blockGroupType: 'ListItem',
blocks: [
{
Expand Down Expand Up @@ -997,22 +997,25 @@ describe(ID, () => {
levels: [
{
listType: 'OL',
format: { marginTop: '1em', listStyleType: 'decimal' },
format: { marginTop: '0cm', listStyleType: 'decimal' },
dataset: { editingInfo: '{"orderedStyleType":1}' },
},
{
listType: 'OL',
format: {
startNumberOverride: 1,
marginTop: '1em',
paddingLeft: '0px',
marginTop: '0cm',
listStyleType: 'lower-alpha',
},
dataset: { editingInfo: '{"orderedStyleType":5}' },
},
],
blockType: 'BlockGroup',
format: { lineHeight: '115%', marginLeft: '72pt' },
format: {
marginTop: '0cm',
marginBottom: '0cm',
lineHeight: '115%',
},
blockGroupType: 'ListItem',
blocks: [
{
Expand Down Expand Up @@ -1051,21 +1054,24 @@ describe(ID, () => {
levels: [
{
listType: 'OL',
format: { marginTop: '1em', listStyleType: 'decimal' },
format: { marginTop: '0cm', listStyleType: 'decimal' },
dataset: { editingInfo: '{"orderedStyleType":1}' },
},
{
listType: 'OL',
format: {
marginTop: '1em',
paddingLeft: '0px',
marginTop: '0cm',
listStyleType: 'lower-alpha',
},
dataset: { editingInfo: '{"orderedStyleType":5}' },
},
],
blockType: 'BlockGroup',
format: { lineHeight: '115%', marginLeft: '72pt' },
format: {
marginTop: '0cm',
marginBottom: '0cm',
lineHeight: '115%',
},
blockGroupType: 'ListItem',
blocks: [
{
Expand Down Expand Up @@ -1104,12 +1110,12 @@ describe(ID, () => {
levels: [
{
listType: 'OL',
format: { marginTop: '1em', listStyleType: 'decimal' },
format: { marginTop: '0cm', listStyleType: 'decimal' },
dataset: { editingInfo: '{"orderedStyleType":1}' },
},
],
blockType: 'BlockGroup',
format: { lineHeight: '115%' },
format: { marginTop: '0cm', marginBottom: '8pt', lineHeight: '115%' },
blockGroupType: 'ListItem',
blocks: [
{
Expand Down
Loading
Loading