Skip to content

Commit 4673e8e

Browse files
committed
HAR-9432 Formatting - Line spacing
1 parent 8fd43ff commit 4673e8e

13 files changed

Lines changed: 262 additions & 33 deletions

File tree

packages/super-editor/src/components/toolbar/super-toolbar.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -349,7 +349,7 @@ export class SuperToolbar extends EventEmitter {
349349
} else {
350350
return item.activate();
351351
}
352-
};
352+
}
353353

354354
const activeMark = marks.find((mark) => mark.name === item.name.value);
355355

packages/super-editor/src/core/helpers/getActiveFormatting.js

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,8 +4,9 @@ import { findMark } from './findMark.js';
44
export function getActiveFormatting(editor) {
55
const { state } = editor;
66
const { selection } = state;
7-
7+
88
const marks = getMarksFromSelection(state);
9+
const markAttrs = selection.$head.parent.attrs.marksAttrs;
910

1011
const marksToProcess = marks
1112
.filter((mark) => !['textStyle', 'link'].includes(mark.type.name))
@@ -14,6 +15,18 @@ export function getActiveFormatting(editor) {
1415
const textStyleMarks = marks.filter((mark) => mark.type.name === 'textStyle');
1516
marksToProcess.push(...textStyleMarks.flatMap(unwrapTextMarks));
1617

18+
// Empty paragraphs could have marks defined as attributes
19+
if (markAttrs) {
20+
const marksFromAttrs = markAttrs
21+
.filter((mark) => !['textStyle', 'link'].includes(mark.type))
22+
.map((mark) => ({ name: mark.type, attrs: mark.attrs || {} }));
23+
24+
const textStyleMarksFromAttrs = markAttrs.filter((mark) => mark.type === 'textStyle');
25+
26+
marksToProcess.push(...marksFromAttrs);
27+
marksToProcess.push(...textStyleMarksFromAttrs.flatMap(unwrapTextMarks));
28+
}
29+
1730
const linkMarkType = state.schema.marks['link'];
1831
const linkMark = findMark(state, linkMarkType);
1932

packages/super-editor/src/core/super-converter/exporter.js

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -167,7 +167,7 @@ function generateParagraphProperties(node) {
167167
const { styleId } = attrs;
168168
if (styleId) pPrElements.push({ name: 'w:pStyle', attributes: { 'w:val': styleId } });
169169

170-
const { spacing, indent, textAlign } = attrs;
170+
const { spacing, indent, textAlign, marksAttrs, keepLines, keepNext } = attrs;
171171
if (spacing) {
172172
const { lineSpaceBefore, lineSpaceAfter, line, lineRule } = spacing;
173173

@@ -187,11 +187,12 @@ function generateParagraphProperties(node) {
187187
}
188188

189189
if (indent) {
190-
const { left, right, firstLine } = indent;
190+
const { left, right, firstLine, hanging } = indent;
191191
const attributes = {};
192192
if (left || left === 0) attributes['w:left'] = pixelsToTwips(left);
193193
if (right || right === 0) attributes['w:right'] = pixelsToTwips(right);
194194
if (firstLine || firstLine === 0) attributes['w:firstLine'] = pixelsToTwips(firstLine);
195+
if (hanging || hanging === 0) attributes['w:hanging'] = pixelsToTwips(hanging);
195196

196197
const indentElement = {
197198
name: 'w:ind',
@@ -208,8 +209,28 @@ function generateParagraphProperties(node) {
208209
pPrElements.push(textAlignElement);
209210
}
210211

211-
if (!pPrElements.length) return null;
212+
if (marksAttrs) {
213+
const outputMarks = processOutputMarks(marksAttrs);
214+
const rPrElement = generateRunProps(outputMarks);
215+
pPrElements.push(rPrElement);
216+
}
217+
218+
if (keepLines) {
219+
pPrElements.push({
220+
name: 'w:keepLines',
221+
attributes: { 'w:val': keepLines },
222+
});
223+
}
224+
225+
if (keepNext) {
226+
pPrElements.push({
227+
name: 'w:keepNext',
228+
attributes: { 'w:val': keepNext },
229+
});
230+
}
212231

232+
if (!pPrElements.length) return null;
233+
213234
return {
214235
name: 'w:pPr',
215236
elements: pPrElements,
@@ -533,16 +554,16 @@ function translateList(params) {
533554
...paragraphNode.attrs,
534555
...listNode.attrs
535556
};
536-
557+
537558
const outputNode = exportSchemaToJson({ ...params, node: paragraphNode });
538559
if (!outputNode.elements) {
539560
outputNode.elements = [];
540561
}
541562
const propsElementIndex = outputNode.elements.findIndex((e) => e.name === 'w:pPr');
542-
563+
543564
const listProps = getListParagraphProperties(listNode, level, type, propsElementIndex > -1);
544565
const content = outputNode.elements.filter((e) => e.name !== 'w:pPr');
545-
566+
546567
if (!content.length) {
547568
// Some empty nodes could have spacing defined
548569
const spacingProp = outputNode.elements[propsElementIndex]?.elements.find((e) => e.name === 'w:spacing');
@@ -562,9 +583,11 @@ function translateList(params) {
562583
if (propsElementIndex === -1) {
563584
outputNode.elements.unshift(listProps);
564585
} else {
565-
outputNode.elements[propsElementIndex].elements.push(listProps);
586+
outputNode.elements[propsElementIndex].elements = [
587+
...outputNode.elements[propsElementIndex].elements,
588+
...listProps
589+
];
566590
}
567-
568591
listNodes.push(outputNode);
569592
});
570593
});
@@ -603,15 +626,24 @@ function getListParagraphProperties(node, level, type, hasParentProps) {
603626
},
604627
],
605628
};
629+
const resultElements = [numPr];
630+
631+
let indent;
632+
if (node.attrs.attributes?.parentAttributes) {
633+
// Keep original list indent
634+
indent = node.attrs.attributes.parentAttributes.paragraphProperties.elements.find(e => e.name === 'w:ind');
635+
}
636+
637+
indent && resultElements.push(indent);
606638

607639
if (hasParentProps) {
608-
return numPr;
640+
return resultElements;
609641
}
610642

611643
return {
612644
name: 'w:pPr',
613645
type: 'element',
614-
elements: [numPr],
646+
elements: resultElements
615647
};
616648
}
617649

packages/super-editor/src/core/super-converter/v2/importer/importerHelpers.js

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,9 @@ export function parseProperties(node, docx) {
1616
const unknownMarks = [];
1717
const { attributes = {}, elements = [] } = node;
1818
const { nodes, paragraphProperties = {}, runProperties = {} } = splitElementsAndProperties(elements);
19-
paragraphProperties.elements = paragraphProperties?.elements?.filter((el) => el.name !== 'w:rPr');
19+
const hasRun = elements.find(element => element.name === 'w:r');
20+
21+
if (hasRun) paragraphProperties.elements = paragraphProperties?.elements?.filter((el) => el.name !== 'w:rPr');
2022

2123
// Get the marks from the run properties
2224
if (runProperties && runProperties?.elements?.length) {
@@ -58,7 +60,7 @@ function splitElementsAndProperties(elements) {
5860
const rPr = elements.find((el) => el.name === 'w:rPr');
5961
const sectPr = elements.find((el) => el.name === 'w:sectPr');
6062
const els = elements.filter((el) => el.name !== 'w:pPr' && el.name !== 'w:rPr' && el.name !== 'w:sectPr');
61-
63+
6264
return {
6365
nodes: els,
6466
paragraphProperties: pPr,

packages/super-editor/src/core/super-converter/v2/importer/listImporter.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -515,7 +515,7 @@ function _processListRunProperties(data) {
515515
elements.forEach((item) => {
516516
if (!expectedTypes.includes(item.name)) {
517517
// console.warn(`[numbering.xml] Unexpected list run prop found: ${item.name}`);
518-
};
518+
}
519519
const { attributes = {} } = item;
520520
Object.keys(attributes).forEach((key) => {
521521
runProperties[key] = attributes[key];

packages/super-editor/src/core/super-converter/v2/importer/paragraphNodeImporter.js

Lines changed: 56 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { twipsToInches, twipsToPixels, twipsToLines } from '../../helpers.js';
1+
import { twipsToInches, twipsToLines, twipsToPixels } from '../../helpers.js';
22
import { testForList } from './listImporter.js';
33
import { carbonCopy } from '../../../utilities/carbonCopy.js';
44
import { mergeTextNodes } from './mergeTextNodes.js';
@@ -49,33 +49,50 @@ export const handleParagraphNode = (params) => {
4949

5050
const pPr = node.elements?.find((el) => el.name === 'w:pPr');
5151
const styleTag = pPr?.elements?.find((el) => el.name === 'w:pStyle');
52+
const nestedRPr = pPr?.elements?.find((el) => el.name === 'w:rPr');
53+
54+
if (nestedRPr) {
55+
schemaNode.attrs.marksAttrs = parseMarks(nestedRPr, []);
56+
}
57+
5258
if (styleTag) {
5359
schemaNode.attrs['styleId'] = styleTag.attributes['w:val'];
5460
}
5561

5662
const indent = pPr?.elements?.find((el) => el.name === 'w:ind');
5763
if (indent && indent.attributes) {
58-
const { 'w:left': left, 'w:right': right, 'w:firstLine': firstLine } = indent?.attributes;
64+
const { 'w:left': left, 'w:right': right, 'w:firstLine': firstLine, 'w:hanging': hanging } = indent?.attributes;
5965

6066
if (schemaNode.attrs) {
6167
if (!schemaNode.attrs.indent) schemaNode.attrs.indent = {};
6268
if (left) schemaNode.attrs['indent'].left = twipsToPixels(left);
6369
if (right) schemaNode.attrs['indent'].right = twipsToPixels(right);
6470
if (firstLine) schemaNode.attrs['indent'].firstLine = twipsToPixels(firstLine);
71+
if (hanging) schemaNode.attrs['indent'].hanging = twipsToPixels(hanging);
6572
}
6673

67-
const textIndentVal = left || firstLine || 0;
74+
const textIndentVal = left - parseInt(hanging || 0) || 0;
6875
schemaNode.attrs['textIndent'] = `${twipsToInches(textIndentVal)}in`;
6976
}
7077

7178
const justify = pPr?.elements?.find((el) => el.name === 'w:jc');
7279
if (justify && justify.attributes) {
7380
schemaNode.attrs['textAlign'] = justify.attributes['w:val'];
7481
}
82+
83+
const keepLines = pPr?.elements?.find((el) => el.name === 'w:keepLines');
84+
if (keepLines && keepLines.attributes) {
85+
schemaNode.attrs['keepLines'] = keepLines.attributes['w:val'];
86+
}
87+
88+
const keepNext = pPr?.elements?.find((el) => el.name === 'w:keepNext');
89+
if (keepNext && keepNext.attributes) {
90+
schemaNode.attrs['keepNext'] = keepNext.attributes['w:val'];
91+
}
7592

7693
if (docx) {
7794
const defaultStyleId = node.attributes?.['w:rsidRDefault'];
78-
schemaNode.attrs['spacing'] = getParagraphSpacing(node, docx);
95+
schemaNode.attrs['spacing'] = getParagraphSpacing(node, docx, schemaNode.attrs['styleId'], schemaNode.attrs.marksAttrs);
7996
schemaNode.attrs['rsidRDefault'] = defaultStyleId;
8097
}
8198

@@ -92,21 +109,24 @@ export const handleParagraphNode = (params) => {
92109
return { nodes: schemaNode ? [schemaNode] : [], consumed: 1 };
93110
};
94111

95-
export const getParagraphSpacing = (node, docx) => {
112+
export const getParagraphSpacing = (node, docx, styleId = '', marks = []) => {
96113
// Check if we have default paragraph styles to override
97114
const spacing = {
98115
lineSpaceAfter: 0,
99116
lineSpaceBefore: 0,
100117
line: 0,
101118
lineRule: null,
102119
}
103-
104-
const { spacing: pDefaultSpacing = {} } = getDefaultParagraphStyle(docx);
120+
121+
const { spacing: pDefaultSpacing = {} } = getDefaultParagraphStyle(docx, styleId);
105122
let lineSpaceAfter, lineSpaceBefore, line, lineRuleStyle;
106123

107124
const pPr = node.elements?.find((el) => el.name === 'w:pPr');
108125
const inLineSpacingTag = pPr?.elements?.find((el) => el.name === 'w:spacing');
109126
const inLineSpacing = inLineSpacingTag?.attributes || {};
127+
128+
const textStyleMark = marks.find((el) => el.type === 'textStyle');
129+
const fontSize = textStyleMark?.attrs?.fontSize;
110130

111131
// These styles are taken in order of precedence
112132
// 1. Inline spacing
@@ -117,17 +137,27 @@ export const getParagraphSpacing = (node, docx) => {
117137

118138
const beforeSpacing = inLineSpacing?.['w:before'] || lineSpaceBefore || pDefaultSpacing?.['w:before'];
119139
if (beforeSpacing) spacing.lineSpaceBefore = twipsToPixels(beforeSpacing);
140+
141+
const beforeAutospacing = inLineSpacing?.['w:beforeAutospacing'];
142+
if (beforeAutospacing === '1' && fontSize) {
143+
spacing.lineSpaceBefore += Math.round((parseInt(fontSize) * 0.5) * 96 / 72);
144+
}
120145

121146
const afterSpacing = inLineSpacing?.['w:after'] || lineSpaceAfter || pDefaultSpacing?.['w:after'];
122147
if (afterSpacing) spacing.lineSpaceAfter = twipsToPixels(afterSpacing);
123148

149+
const afterAutospacing = inLineSpacing?.['w:afterAutospacing'];
150+
if (afterAutospacing === '1' && fontSize) {
151+
spacing.lineSpaceAfter += Math.round((parseInt(fontSize) * 0.5) * 96 / 72);
152+
}
153+
124154
const lineRule = inLineSpacing?.['w:lineRule'] || lineRuleStyle || pDefaultSpacing?.['w:lineRule'];
125155
if (lineRule) spacing.lineRule = lineRule;
126156

127157
return spacing;
128158
};
129159

130-
const getDefaultParagraphStyle = (docx) => {
160+
const getDefaultParagraphStyle = (docx, styleId = '') => {
131161
const styles = docx['word/styles.xml'];
132162
if (!styles) {
133163
return {};
@@ -136,10 +166,26 @@ const getDefaultParagraphStyle = (docx) => {
136166
const pDefault = defaults.elements.find((el) => el.name === 'w:pPrDefault');
137167
const pPrDefault = pDefault?.elements?.find((el) => el.name === 'w:pPr');
138168
const pPrDefaultSpacingTag = pPrDefault?.elements?.find((el) => el.name === 'w:spacing') || {};
169+
170+
// Paragraph 'Normal' styles
171+
const stylesNormal = styles.elements[0].elements?.find((el) => el.name === 'w:style' && el.attributes['w:styleId'] === 'Normal');
172+
const pPrNormal = stylesNormal?.elements?.find((el) => el.name === 'w:pPr');
173+
const pPrNormalSpacingTag = pPrNormal?.elements?.find((el) => el.name === 'w:spacing') || {};
174+
175+
// Styles based on styleId
176+
let pPrStyleIdSpacingTag = {};
177+
if (styleId) {
178+
const stylesById = styles.elements[0].elements?.find((el) => el.name === 'w:style' && el.attributes['w:styleId'] === styleId);
179+
const pPrById = stylesById?.elements?.find((el) => el.name === 'w:pPr');
180+
pPrStyleIdSpacingTag = pPrById?.elements?.find((el) => el.name === 'w:spacing') || {};
181+
}
182+
139183
const { attributes: pPrDefaultSpacingAttr } = pPrDefaultSpacingTag;
184+
const { attributes: pPrNormalSpacingAttr } = pPrNormalSpacingTag;
185+
const { attributes: pPrByIdSpacingAttr } = pPrStyleIdSpacingTag;
140186

141187
return {
142-
spacing: pPrDefaultSpacingAttr,
188+
spacing: pPrByIdSpacingAttr || pPrDefaultSpacingAttr || pPrNormalSpacingAttr,
143189
}
144190
};
145191

@@ -323,6 +369,6 @@ export function preProcessNodesForFldChar(nodes) {
323369
});
324370
}
325371
});
326-
372+
327373
return processedNodes;
328374
}

packages/super-editor/src/extensions/linked-styles/linked-styles.js

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,6 +196,31 @@ export const getSpacingStyleString = (spacing) => {
196196
`.trim();
197197
};
198198

199+
export const getMarksStyle = (attrs) => {
200+
let styles = '';
201+
for (const attr of attrs) {
202+
switch (attr.type) {
203+
case 'bold':
204+
styles += `font-weight: bold; `;
205+
break;
206+
case 'italic':
207+
styles += `font-style: italic; `;
208+
break;
209+
case 'underline':
210+
styles += `text-decoration: underline; `;
211+
break;
212+
case 'highlight':
213+
styles += `background-color: ${attr.attrs.color}; `;
214+
break;
215+
case 'textStyle':
216+
const { fontFamily, fontSize } = attr.attrs;
217+
styles += `${fontFamily ? `font-family: ${fontFamily};` : ''} ${fontSize ? `font-size: ${fontSize};` : ''}`;
218+
}
219+
}
220+
221+
return styles.trim();
222+
}
223+
199224
export const getQuickFormatList = (editor) => {
200225
if (!editor?.converter) return [];
201226
const styles = editor.converter.linkedStyles || [];
@@ -206,4 +231,4 @@ export const getQuickFormatList = (editor) => {
206231
.sort((a, b) => {
207232
return a.definition.attrs?.name.localeCompare(b.definition.attrs?.name);
208233
});
209-
};
234+
};

0 commit comments

Comments
 (0)