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 @@ -51,9 +51,11 @@ class SuperConverter {
{ name: 'w:sz', type: 'fontSize', mark: 'textStyle', property: 'fontSize' },
// { name: 'w:szCs', type: 'fontSize', mark: 'textStyle', property: 'fontSize' },
{ name: 'w:rFonts', type: 'fontFamily', mark: 'textStyle', property: 'fontFamily' },
{ name: 'w:rStyle', type: 'styleId', mark: 'textStyle', property: 'styleId' },
{ name: 'w:jc', type: 'textAlign', mark: 'textStyle', property: 'textAlign' },
{ name: 'w:ind', type: 'textIndent', mark: 'textStyle', property: 'textIndent' },
{ name: 'w:spacing', type: 'lineHeight', mark: 'textStyle', property: 'lineHeight' },
{ name: 'w:spacing', type: 'letterSpacing', mark: 'textStyle', property: 'letterSpacing' },
{ name: 'link', type: 'link', mark: 'link', property: 'href' },
{ name: 'w:highlight', type: 'highlight', mark: 'highlight', property: 'color' },
{ name: 'w:shd', type: 'highlight', mark: 'highlight', property: 'color' },
Expand Down
7 changes: 6 additions & 1 deletion packages/super-editor/src/core/super-converter/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -1430,6 +1430,12 @@ function translateMark(mark) {
});
break;

// Add ability to get run styleIds from textStyle marks and inject to run properties in word
case 'styleId':
markElement.name = 'w:rStyle';
markElement.attributes['w:val'] = attrs.styleId;
break;

case 'color':
let processedColor = attrs.color.replace(/^#/, '').replace(/;$/, ''); // Remove `#` and `;` if present
if (processedColor.startsWith('rgb')) {
Expand Down Expand Up @@ -1458,7 +1464,6 @@ function translateMark(mark) {
case 'lineHeight':
markElement.attributes['w:line'] = linesToTwips(attrs.lineHeight);
break;

case 'highlight':
markElement.attributes['w:fill'] = attrs.color?.substring(1);
markElement.attributes['w:color'] = 'auto';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,27 @@ export function parseMarks(property, unknownMarks = [], docx = null) {
}
}

marksForType.forEach((m) => {
let filteredMarksForType = marksForType;

/**
* Now that we have 2 marks named 'spacing' we need to determine if its
* for line height or letter spacing.
*
* If the spacing has a w:val attribute, it's for letter spacing.
* If the spacing has a w:line, w:lineRule, w:before, w:after attribute, it's for line height.
*/
if (element.name === 'w:spacing') {
const attrs = element.attributes || {};
const hasLetterSpacing = attrs['w:val'];
filteredMarksForType = marksForType.filter((m) => {
if (hasLetterSpacing) {
return m.type === 'letterSpacing';
}
return m.type === 'lineHeight';
});
}

filteredMarksForType.forEach((m) => {
if (!m || seen.has(m.type)) return;
seen.add(m.type);

Expand Down Expand Up @@ -145,6 +165,7 @@ function getMarkValue(markType, attributes, docx) {
textIndent: () => getIndentValue(attributes),
fontFamily: () => getFontFamilyValue(attributes, docx),
lineHeight: () => getLineHeightValue(attributes),
letterSpacing: () => `${twipsToPt(attributes['w:val'])}pt`,
textAlign: () => attributes['w:val'],
link: () => attributes['href'],
underline: () => attributes['w:val'],
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
/**
* @type {import("docxImporter").NodeHandler}
*/
const handleRunNode = (params) => {
export const handleRunNode = (params) => {
const { nodes, nodeListHandler, parentStyleId, docx } = params;
if (nodes.length === 0 || nodes[0].name !== 'w:r') {
return { nodes: [], consumed: 0 };
Expand All @@ -17,27 +17,75 @@
const defaultNodeStyles = getMarksFromStyles(docx, parentStyleId);

if (hasRunProperties) {
const { marks = [], attributes = {} } = parseProperties(node);

Check warning on line 20 in packages/super-editor/src/core/super-converter/v2/importer/runNodeImporter.js

View workflow job for this annotation

GitHub Actions / Lint & Format Check

'attributes' is assigned a value but never used

// Apply fonts from related style definition if there is no marks
const textStyleMark = marks.find((m) => m.type === 'textStyle');
const hasFontStyle = textStyleMark && Object.keys(textStyleMark.attrs).length > 0;
if (defaultNodeStyles.marks && !hasFontStyle) {
const hasBoldDisabled = marks.find((m) => m.type === 'bold')?.attrs?.value === '0';
for (let mark of defaultNodeStyles.marks) {
if (['bold'].includes(mark.type) && hasBoldDisabled) continue;
marks.push(mark);
/* Store run style attributes in an array, then store the defaultNodeStyles (parent styles) in a second array
Then combine the two arrays and create a new array of marks, where the
run style attributes override the defaultNodeStyles

*/
// Collect run style attributes
let runStyleAttributes = [];
const runStyleElement = node.elements
?.find((el) => el.name === 'w:rPr')
?.elements?.find((el) => el.name === 'w:rStyle');
let runStyleId;
if (runStyleElement && runStyleElement.attributes?.['w:val'] && docx) {
runStyleId = runStyleElement.attributes['w:val'];
const runStyleDefinition = getMarksFromStyles(docx, runStyleId);
if (runStyleDefinition.marks && runStyleDefinition.marks.length > 0) {
runStyleAttributes = runStyleDefinition.marks;
}
}

if (node.marks) marks.push(...node.marks);
const newMarks = createImportMarks(marks);
// Collect paragraph style attributes
let paragraphStyleAttributes = [];
if (defaultNodeStyles.marks) {
// Filter out bold if it's disabled
paragraphStyleAttributes = defaultNodeStyles.marks.filter((mark) => {
if (['bold'].includes(mark.type) && marks.find((m) => m.type === 'bold')?.attrs?.value === '0') {
return false;
}
return true;
});
}

// Combine with correct precedence: paragraph styles first, then run styles (which override)
const combinedMarks = [...paragraphStyleAttributes];

// Add run style attributes if they don't already exist
runStyleAttributes.forEach((runStyle) => {
const exists = combinedMarks.some(
(mark) =>
mark.type === runStyle.type && JSON.stringify(mark.attrs || {}) === JSON.stringify(runStyle.attrs || {}),
);
if (!exists) {
combinedMarks.push(runStyle);
}
});

// Add direct marks if they don't already exist
marks.forEach((mark) => {
const exists = combinedMarks.some(
(existing) =>
existing.type === mark.type && JSON.stringify(existing.attrs || {}) === JSON.stringify(mark.attrs || {}),
);
if (!exists) {
combinedMarks.push(mark);
}
});
// Attach the originating run style id so the span gets styleid like paragraph nodes
if (runStyleId) combinedMarks.push({ type: 'textStyle', attrs: { styleId: runStyleId } });

if (node.marks) combinedMarks.push(...node.marks);
const newMarks = createImportMarks(combinedMarks);
processedRun = processedRun.map((n) => {
const existingMarks = n.marks || [];
return { ...n, marks: [...newMarks, ...existingMarks], attributes };
return {
...n,
marks: [...newMarks, ...existingMarks],
};
});
} else if (defaultNodeStyles.marks) {
processedRun = processedRun.map((n) => ({ ...n, marks: createImportMarks(defaultNodeStyles.marks) }));
}
return { nodes: processedRun, consumed: 1 };
};
Expand All @@ -53,7 +101,7 @@

if (!style) return {};

return parseProperties(style, docx);
return parseProperties(style);
};

/**
Expand Down
8 changes: 7 additions & 1 deletion packages/super-editor/src/extensions/linked-styles/plugin.js
Original file line number Diff line number Diff line change
Expand Up @@ -50,11 +50,17 @@ const generateDecorations = (state, styles) => {
doc.descendants((node, pos) => {
const { name } = node.type;

// Track the current StyleId
if (node?.attrs?.styleId) lastStyleId = node.attrs.styleId;
if (name === 'paragraph' && !node.attrs?.styleId) lastStyleId = null;
if (name !== 'text' && name !== 'listItem' && name !== 'orderedList') return;

// Get the last styleId from the node marks
// This allows run-level styles and styleIds to override paragraph-level styles
for (const mark of node.marks) {
if (mark.type.name === 'textStyle' && mark.attrs.styleId) {
lastStyleId = mark.attrs.styleId;
}
}
const { linkedStyle, basedOnStyle } = getLinkedStyle(lastStyleId, styles);
if (!linkedStyle) return;

Expand Down
6 changes: 6 additions & 0 deletions packages/super-editor/src/extensions/text-style/text-style.js
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,12 @@ export const TextStyle = Mark.create({
return ['span', Attribute.mergeAttributes(this.options.htmlAttributes, htmlAttributes), 0];
},

addAttributes() {
return {
styleId: {},
};
},

addCommands() {
return {
removeEmptyTextStyle:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,9 @@ describe('HyperlinkNodeExporter', async () => {
);

const rPr = hyperLinkNode.elements[0].elements[0];
expect(rPr.elements[1].name).toBe('w:u');
expect(rPr.elements[1].attributes['w:val']).toBe('single');
expect(rPr.elements[0].name).toBe('w:u');
expect(rPr.elements[0].attributes['w:val']).toBe('single');
expect(rPr.elements[1].name).toBe('w:color');
expect(rPr.elements[2].name).toBe('w:rFonts');
expect(rPr.elements[2].attributes['w:ascii']).toBe('Arial');
expect(rPr.elements[3].name).toBe('w:sz');
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@ describe('HyperlinkNodeImporter', () => {
nodeListHandler: defaultNodeListHandler(),
});
const { marks } = nodes[0];

expect(marks.length).toBe(3);
expect(marks[0].type).toBe('underline');
expect(marks[1].type).toBe('link');
Expand All @@ -30,5 +29,12 @@ describe('HyperlinkNodeImporter', () => {
'https://stackoverflow.com/questions/66669593/how-to-attach-image-at-first-page-in-docx-file-nodejs',
);
expect(marks[1].attrs.rId).toBe('rId4');

// Capture the textStyle mark
const textStyleMark = marks[2];
expect(textStyleMark.type).toBe('textStyle');
expect(textStyleMark.attrs.styleId).toBe('Hyperlink');
expect(textStyleMark.attrs.fontFamily).toBe('Arial');
expect(textStyleMark.attrs.fontSize).toBe('10pt');
});
});
Loading
Loading