Skip to content

Commit 7c782b8

Browse files
committed
fix lists and linked styles
1 parent 6ab4515 commit 7c782b8

7 files changed

Lines changed: 203 additions & 11 deletions

File tree

packages/super-editor/src/assets/styles/extensions/list-items.css

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,8 @@
1818
min-height: inherit;
1919
word-wrap: break-word;
2020
}
21+
22+
/* temporary fix */
23+
.sd-editor-list-item-node-view .sd-custom-selection {
24+
font-size: inherit!important;
25+
}

packages/super-editor/src/components/toolbar/LinkedStyle.vue

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -72,7 +72,7 @@ onMounted(() => {
7272
>
7373
<div
7474
class="style-name"
75-
:style="generateLinkedStyleString(style, null, false)"
75+
:style="generateLinkedStyleString(style, null, null, false)"
7676
data-item="btn-linkedStyles-option"
7777
>
7878
{{ style.definition.attrs.name }}

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

Lines changed: 146 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ export const handleListNode = (params) => {
2222
if (isList) {
2323
node.isList = true;
2424
const result = handleListNodes(params, node);
25-
2625
return {
2726
nodes: [result],
2827
consumed: 1,
@@ -71,9 +70,15 @@ function handleListNodes(params, node) {
7170
// If no iLvl, try an outline level
7271
if (!iLvl && iLvl !== 0) iLvl = getOutlineLevelFromStyleTag(styleId, docx);
7372

74-
const { listType, listOrderingType, listrPrs, listpPrs, start, lvlText, lvlJc, customFormat } =
75-
getNodeNumberingDefinition(node, iLvl, docx);
73+
let numberingDefinition = getNodeNumberingDefinition(node, iLvl, docx);
74+
if (!Object.keys(numberingDefinition).length) {
75+
const { definition, ilvl } = getNodeNumberingDefinitionByStyle(node, docx);
76+
if (definition) numberingDefinition = definition;
77+
if (Number.isNaN(iLvl)) iLvl = ilvl;
78+
}
7679

80+
const { listType, listOrderingType, listrPrs, listpPrs, start, lvlText, lvlJc, customFormat } = numberingDefinition;
81+
7782
// Fallback if the list definition is not found or is invalid
7883
// See invalid-list-def-fallback.docx for example and
7984
if (!listType) {
@@ -196,6 +201,7 @@ export function testForList(node, docx) {
196201
let outlinelvl;
197202

198203
const styleId = paragraphStyle?.attributes['w:val'];
204+
199205
const styleTag = getStyleTagFromStyleId(styleId, docx);
200206
if (styleTag && !numId) {
201207
const { numPr: numPrRecursve, type } = getNumPrRecursive({ node, styleId, docx });
@@ -212,6 +218,11 @@ export function testForList(node, docx) {
212218
const levelDefinition = abstractNumDefinition?.elements?.find(
213219
(el) => el.name === 'w:lvl' && el.attributes?.['w:ilvl'] == ilvl,
214220
);
221+
222+
if (numId && !levelDefinition && abstractNumDefinition) {
223+
return true;
224+
}
225+
215226
if (!levelDefinition) return false;
216227

217228
return !!numId;
@@ -478,7 +489,7 @@ export function normalizeLvlTextChar(lvlText) {
478489
* @returns
479490
*/
480491
export function getNodeNumberingDefinition(item, level, docx) {
481-
if (!item) return;
492+
if (!item) return {};
482493
const { attributes = {} } = item;
483494

484495
const { paragraphProperties = {} } = attributes;
@@ -525,6 +536,137 @@ export function getNodeNumberingDefinition(item, level, docx) {
525536
return { listType, listOrderingType: listTypeDef, listrPrs, listpPrs, start, lvlText, lvlJc, customFormat };
526537
}
527538

539+
export function getNodeNumberingDefinitionByStyle(item, docx) {
540+
if (!item) return {};
541+
542+
const initialPpr = item.elements?.find((el) => el.name === 'w:pPr');
543+
const styleTag = initialPpr?.elements?.find((el) => el.name === 'w:pStyle');
544+
const styleId = styleTag?.attributes['w:val'];
545+
const styleDef = getStyleTagFromStyleId(styleId, docx);
546+
if (!styleDef) return {};
547+
548+
const pPr = styleDef.elements?.find((el) => el.name === 'w:pPr');
549+
const numPr = pPr?.elements?.find((el) => el.name === 'w:numPr');
550+
const numIdTag = numPr?.elements?.find((el) => el.name === 'w:numId');
551+
const numId = numIdTag?.attributes?.['w:val'];
552+
if (!numId) return {};
553+
554+
const abstractNumId = getAbstractNumIdByNumId(numId, docx);
555+
if (!abstractNumId) return {};
556+
557+
const levelData = getLevelDataFromAbstractNum(abstractNumId, styleId, docx);
558+
if (!levelData) return {};
559+
560+
const definition = extractDefinitionFromLevel(levelData.level, initialPpr);
561+
562+
return {
563+
definition,
564+
ilvl: levelData.ilvl,
565+
};
566+
}
567+
568+
function getAbstractNumIdByNumId(numId, docx) {
569+
const numbering = docx['word/numbering.xml'];
570+
if (!numbering) return null;
571+
572+
const { elements } = numbering;
573+
const listData = elements[0];
574+
const numberingElements = listData.elements || [];
575+
576+
const numDef = numberingElements.find((el) =>
577+
el.name === 'w:num' &&
578+
el.attributes?.['w:numId'] === numId
579+
);
580+
581+
if (!numDef) return null;
582+
583+
const abstractNumIdRef = numDef.elements?.find((el) => el.name === 'w:abstractNumId');
584+
return abstractNumIdRef?.attributes?.['w:val'];
585+
}
586+
587+
function getLevelDataFromAbstractNum(abstractNumId, styleId, docx) {
588+
const numbering = docx['word/numbering.xml'];
589+
if (!numbering) return null;
590+
591+
const { elements } = numbering;
592+
const listData = elements[0];
593+
const numberingElements = listData.elements || [];
594+
595+
const abstractNum = numberingElements.find((el) =>
596+
el.name === 'w:abstractNum' &&
597+
el.attributes?.['w:abstractNumId'] === abstractNumId
598+
);
599+
600+
if (!abstractNum) return null;
601+
602+
const levels = abstractNum.elements?.filter(el => el.name === 'w:lvl') || [];
603+
for (const level of levels) {
604+
const pStyle = level.elements?.find((el) => el.name === 'w:pStyle');
605+
if (pStyle?.attributes?.['w:val'] === styleId) {
606+
const found = {
607+
level,
608+
ilvl: Number(level.attributes?.['w:ilvl']) || 0
609+
};
610+
return found;
611+
}
612+
}
613+
614+
const level0 = levels.find((level) => level.attributes?.['w:ilvl'] === '0');
615+
if (level0) {
616+
return {
617+
level: level0,
618+
ilvl: 0,
619+
};
620+
}
621+
622+
return null;
623+
}
624+
625+
function extractDefinitionFromLevel(level, initialPpr) {
626+
if (!level) return {};
627+
628+
const start = level.elements?.find((el) => el.name === 'w:start')?.attributes?.['w:val'];
629+
630+
let numFmtTag = level.elements?.find((el) => el.name === 'w:numFmt');
631+
let numFmt = numFmtTag?.attributes?.['w:val'];
632+
633+
let lvlText = level.elements?.find((el) => el.name === 'w:lvlText')?.attributes?.['w:val'];
634+
lvlText = normalizeLvlTextChar(lvlText);
635+
636+
let customFormat;
637+
if (numFmt === 'custom') customFormat = numFmtTag?.attributes?.['w:format'];
638+
639+
const lvlJc = level.elements?.find((el) => el.name === 'w:lvlJc')?.attributes?.['w:val'];
640+
const pPr = level.elements?.find((el) => el.name === 'w:pPr');
641+
const rPr = level.elements?.find((el) => el.name === 'w:rPr');
642+
643+
let listpPrs, listrPrs;
644+
if (pPr) listpPrs = _processListParagraphProperties(pPr, initialPpr);
645+
if (rPr) listrPrs = _processListRunProperties(rPr);
646+
647+
let listType;
648+
if (unorderedListTypes.includes(numFmt?.toLowerCase())) {
649+
listType = 'bulletList';
650+
} else if (orderedListTypes.includes(numFmt)) {
651+
listType = 'orderedList';
652+
} else if (numFmt === 'custom') {
653+
listType = 'orderedList';
654+
} else {
655+
return {};
656+
}
657+
658+
return {
659+
listType,
660+
listOrderingType: numFmt,
661+
listrPrs,
662+
listpPrs,
663+
start,
664+
lvlText,
665+
lvlJc,
666+
customFormat,
667+
};
668+
}
669+
528670
export function getDefinitionForLevel(data, level) {
529671
return data?.elements?.find((item) => Number(item.attributes['w:ilvl']) === level);
530672
}

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

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,13 @@ export const handleParagraphNode = (params) => {
119119
}
120120
}
121121

122+
if (docx) {
123+
const { textCase } = getDefaultParagraphStyle(docx, styleId);
124+
if (textCase) {
125+
schemaNode.attrs.textCase = textCase;
126+
}
127+
}
128+
122129
if (framePr && framePr.attributes['w:dropCap']) {
123130
schemaNode.attrs.dropcap = {
124131
type: framePr.attributes['w:dropCap'],
@@ -262,12 +269,22 @@ const getDefaultParagraphStyle = (docx, styleId = '') => {
262269
let pPrStyleIdSpacingTag = {};
263270
let pPrStyleIdIndentTag = {};
264271
let pPrStyleJc = {};
272+
let textCase = null;
265273
if (styleId) {
266274
const stylesById = styles.elements[0].elements?.find(
267275
(el) => el.name === 'w:style' && el.attributes['w:styleId'] === styleId,
268276
);
269277
const pPrById = stylesById?.elements?.find((el) => el.name === 'w:pPr');
270278

279+
const basedOn = stylesById?.elements.find((el) => el.name === 'w:basedOn');
280+
const baseStyles = styles.elements[0].elements?.find(
281+
(el) => el.name === 'w:style' && el.attributes['w:styleId'] === basedOn?.attributes['w:val']
282+
);
283+
const rprBaseStyles = baseStyles?.elements?.find((el) => el.name === 'w:rPr');
284+
285+
const caps = rprBaseStyles?.elements?.find((el) => el.name === 'w:caps');
286+
if (caps) textCase = 'uppercase';
287+
271288
pPrStyleIdSpacingTag = pPrById?.elements?.find((el) => el.name === 'w:spacing') || {};
272289
pPrStyleIdIndentTag = pPrById?.elements?.find((el) => el.name === 'w:ind') || {};
273290
pPrStyleJc = pPrById?.elements?.find((el) => el.name === 'w:jc') || {};
@@ -286,6 +303,7 @@ const getDefaultParagraphStyle = (docx, styleId = '') => {
286303
spacing: pPrByIdSpacingAttr || pPrDefaultSpacingAttr || pPrNormalSpacingAttr,
287304
indent: pPrByIdIndentAttr || pPrDefaultIndentAttr || pPrNormalIndentAttr,
288305
justify: pPrByIdJcAttr,
306+
textCase,
289307
};
290308
};
291309

@@ -366,6 +384,10 @@ export function getDefaultStyleDefinition(defaultStyleId, docx) {
366384
else pageBreakAfterVal = Number(pageBreakAfter?.attributes?.['w:val']);
367385
}
368386

387+
const basedOn = elementsWithId
388+
.find((el) => el.elements.some((inner) => inner.name === 'w:basedOn'))
389+
?.elements.find((inner) => inner.name === 'w:basedOn')?.attributes['w:val'];
390+
369391
const parsedAttrs = {
370392
name,
371393
qFormat: qFormat ? true : false,
@@ -374,6 +396,7 @@ export function getDefaultStyleDefinition(defaultStyleId, docx) {
374396
outlineLevel: outlineLevel ? parseInt(outlineLvlValue) : null,
375397
pageBreakBefore: pageBreakBeforeVal ? true : false,
376398
pageBreakAfter: pageBreakAfterVal ? true : false,
399+
basedOn: basedOn ?? null,
377400
};
378401

379402
// rPr

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

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -96,13 +96,13 @@ const generateDecorations = (state, styles) => {
9696
if (name === 'paragraph' && !node.attrs?.styleId) lastStyleId = null;
9797
if (name !== 'text' && name !== 'listItem' && name !== 'orderedList') return;
9898

99-
const linkedStyle = getLinkedStyle(lastStyleId, styles);
99+
const { linkedStyle, basedOnStyle } = getLinkedStyle(lastStyleId, styles);
100100
if (!linkedStyle) return;
101101

102102
const $pos = state.doc.resolve(pos);
103103
const parent = $pos.parent;
104104

105-
const styleString = generateLinkedStyleString(linkedStyle, node, parent);
105+
const styleString = generateLinkedStyleString(linkedStyle, basedOnStyle, node, parent);
106106
if (!styleString) return;
107107

108108
const decoration = Decoration.inline(pos, pos + node.nodeSize, { style: styleString });
@@ -116,15 +116,24 @@ const generateDecorations = (state, styles) => {
116116
* If the node contains a given mark, we don't override it with the linked style per MS Word behavior
117117
*
118118
* @param {Object} linkedStyle The linked style object
119+
* @param {Object} basedOnStyle The basedOn style object
119120
* @param {Object} node The current node
120121
* @param {Object} parent The parent of current
121122
* @returns {String} The style string
122123
*/
123-
export const generateLinkedStyleString = (linkedStyle, node, parent, includeSpacing = true) => {
124+
export const generateLinkedStyleString = (linkedStyle, basedOnStyle, node, parent, includeSpacing = true) => {
124125
if (!linkedStyle?.definition?.styles) return '';
125126
const markValue = {};
126127

127-
Object.entries(linkedStyle.definition.styles).forEach(([k, value]) => {
128+
const linkedDefinitionStyles = { ...linkedStyle.definition.styles };
129+
const basedOnDefinitionStyles = { ...basedOnStyle?.definition?.styles };
130+
const resultStyles = { ...linkedDefinitionStyles };
131+
132+
if (!linkedDefinitionStyles['font-size'] && basedOnDefinitionStyles['font-size']) {
133+
resultStyles['font-size'] = basedOnDefinitionStyles['font-size'];
134+
}
135+
136+
Object.entries(resultStyles).forEach(([k, value]) => {
128137
const key = kebabCase(k);
129138
const flattenedMarks = [];
130139

@@ -182,7 +191,10 @@ export const generateLinkedStyleString = (linkedStyle, node, parent, includeSpac
182191
* @returns {Object} The linked style
183192
*/
184193
const getLinkedStyle = (styleId, styles = []) => {
185-
return styles.find((style) => style.id === styleId);
194+
const linkedStyle = styles.find((style) => style.id === styleId);
195+
const basedOn = linkedStyle?.definition?.attrs?.basedOn;
196+
const basedOnStyle = styles.find((style) => style.id === basedOn);
197+
return { linkedStyle, basedOnStyle };
186198
};
187199

188200
export const getSpacingStyle = (spacing) => {

packages/super-editor/src/extensions/list-item/ListItemNodeView.js

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -221,7 +221,9 @@ const getStylesFromLinkedStyles = ({ node, pos, editor }) => {
221221
const { state } = editor.view;
222222
const linkedStyles = LinkedStylesPluginKey.getState(state)?.decorations;
223223
const decorationsInPlace = linkedStyles?.find(pos, pos + node.nodeSize);
224-
const styleDeco = decorationsInPlace?.find((dec) => dec.type.attrs?.style);
224+
// We are looking from the end as there may be several decorations
225+
// and we need to find the most specific one.
226+
const styleDeco = decorationsInPlace?.findLast((dec) => dec.type.attrs?.style);
225227
const style = styleDeco?.type.attrs?.style;
226228

227229
const stylesArray = style?.split(';') || [];

packages/super-editor/src/extensions/paragraph/paragraph.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -110,6 +110,14 @@ export const Paragraph = Node.create({
110110
return { style };
111111
},
112112
},
113+
textCase: {
114+
renderDOM: (attrs) => {
115+
if (!attrs.textCase) return {};
116+
return {
117+
style: `text-transform: ${attrs.textCase}`,
118+
}
119+
},
120+
},
113121
};
114122
},
115123

0 commit comments

Comments
 (0)