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 @@ -340,6 +340,11 @@ https://github.com/ProseMirror/prosemirror-tables/blob/master/demo/index.html
z-index: -1;
}

.ProseMirror div[data-horizontal-rule='true'] {
margin-top: auto;
align-self: flex-end;
}

.sd-editor-dropcap {
float: left;
display: flex;
Expand Down
60 changes: 55 additions & 5 deletions packages/super-editor/src/core/super-converter/exporter.js
Original file line number Diff line number Diff line change
Expand Up @@ -2118,8 +2118,14 @@ function translateShapeTextbox(params) {

function translateContentBlock(params) {
const { node } = params;
const { drawingContent } = node.attrs;
const { drawingContent, vmlAttributes, horizontalRule } = node.attrs;

// Handle VML v:rect elements (like horizontal rules)
if (vmlAttributes || horizontalRule) {
return translateVRectContentBlock(params);
}

// Handle modern DrawingML content (existing logic)
const drawing = {
name: 'w:drawing',
elements: [...(drawingContent ? [...(drawingContent.elements || [])] : [])],
Expand All @@ -2136,12 +2142,56 @@ function translateContentBlock(params) {
elements: [choice],
};

const par = {
name: 'w:p',
elements: [wrapTextInRun(alternateContent)],
return wrapTextInRun(alternateContent);
}

function translateVRectContentBlock(params) {
const { node } = params;
const { vmlAttributes, background, attributes, style } = node.attrs;

const rectAttrs = {
id: attributes?.id || `_x0000_i${Math.floor(Math.random() * 10000)}`,
};

return par;
if (style) {
rectAttrs.style = style;
}

if (background) {
rectAttrs.fillcolor = background;
}

if (vmlAttributes) {
if (vmlAttributes.hralign) rectAttrs['o:hralign'] = vmlAttributes.hralign;
if (vmlAttributes.hrstd) rectAttrs['o:hrstd'] = vmlAttributes.hrstd;
if (vmlAttributes.hr) rectAttrs['o:hr'] = vmlAttributes.hr;
if (vmlAttributes.stroked) rectAttrs.stroked = vmlAttributes.stroked;
}

if (attributes) {
Object.entries(attributes).forEach(([key, value]) => {
if (!rectAttrs[key] && value !== undefined) {
rectAttrs[key] = value;
}
});
}

// Create the v:rect element
const rect = {
name: 'v:rect',
attributes: rectAttrs,
};

// Wrap in w:pict
const pict = {
name: 'w:pict',
attributes: {
'w14:anchorId': Math.floor(Math.random() * 0xffffffff).toString(),
},
elements: [rect],
};

return wrapTextInRun(pict);
}

export class DocxExporter {
Expand Down
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { handleParagraphNode } from './paragraphNodeImporter.js';
import { defaultNodeListHandler } from './docxImporter.js';
import { twipsToPixels, twipsToLines } from '../../helpers.js';

export const handlePictNode = (params) => {
const { nodes } = params;
Expand All @@ -20,6 +21,18 @@ export const handlePictNode = (params) => {
const node = pict;
const shape = node.elements?.find((el) => el.name === 'v:shape');
const group = node.elements?.find((el) => el.name === 'v:group');
const rect = node.elements?.find((el) => el.name === 'v:rect');

// Handle v:rect elements (like horizontal rules)
if (rect) {
const result = handleVRectImport({
pict,
pNode,
rect,
params,
});
return { nodes: result ? [result] : [], consumed: 1 };
}

// such a case probably shouldn't exist.
if (!shape && !group) {
Expand Down Expand Up @@ -50,6 +63,89 @@ export const handlePictNode = (params) => {
return { nodes: result ? [result] : [], consumed: 1 };
};

// Handler for v:rect elements
export function handleVRectImport({ rect, pNode }) {
const schemaAttrs = {};
const rectAttrs = rect.attributes || {};

// Store all the attributes you specified
schemaAttrs.attributes = rectAttrs;

// Parse style attribute
if (rectAttrs.style) {
const parsedStyle = parseInlineStyles(rectAttrs.style);
const rectStyle = buildVRectStyles(parsedStyle);

if (rectStyle) {
schemaAttrs.style = rectStyle;
}

// Extract dimensions for the size attribute
const size = {};
if (parsedStyle.width !== undefined) {
size.width = parsePointsToPixels(parsedStyle.width);

// Check for full page width identifier and adjust width to be 100%
if (rectAttrs['o:hr'] === 't') {
size.width = '100%';
}
}
if (parsedStyle.height !== undefined) {
size.height = parsePointsToPixels(parsedStyle.height);
}
if (Object.keys(size).length > 0) {
schemaAttrs.size = size;
}
}

// Handle fillcolor
if (rectAttrs.fillcolor) {
schemaAttrs.background = rectAttrs.fillcolor;
}

// Store VML-specific attributes
const vmlAttrs = {};
if (rectAttrs['o:hralign']) vmlAttrs.hralign = rectAttrs['o:hralign'];
if (rectAttrs['o:hrstd']) vmlAttrs.hrstd = rectAttrs['o:hrstd'];
if (rectAttrs['o:hr']) vmlAttrs.hr = rectAttrs['o:hr'];
if (rectAttrs.stroked) vmlAttrs.stroked = rectAttrs.stroked;

if (Object.keys(vmlAttrs).length > 0) {
schemaAttrs.vmlAttributes = vmlAttrs;
}

// Determine if this is a horizontal rule
const isHorizontalRule = rectAttrs['o:hr'] === 't' || rectAttrs['o:hrstd'] === 't';
if (isHorizontalRule) {
schemaAttrs.horizontalRule = true;
}

const pPr = pNode.elements?.find((el) => el.name === 'w:pPr');
const spacingElement = pPr?.elements?.find((el) => el.name === 'w:spacing');
const spacingAttrs = spacingElement?.attributes || {};

// Parse spacing using the same logic as paragraphNodeImporter
const spacing = {};
if (spacingAttrs['w:after']) spacing.lineSpaceAfter = twipsToPixels(spacingAttrs['w:after']);
if (spacingAttrs['w:before']) spacing.lineSpaceBefore = twipsToPixels(spacingAttrs['w:before']);
if (spacingAttrs['w:line']) spacing.line = twipsToLines(spacingAttrs['w:line']);
if (spacingAttrs['w:lineRule']) spacing.lineRule = spacingAttrs['w:lineRule'];

return {
type: 'paragraph',
content: [
{
type: 'contentBlock',
attrs: schemaAttrs,
},
],
attrs: {
spacing: Object.keys(spacing).length > 0 ? spacing : undefined,
rsidRDefault: pNode.attributes?.['w:rsidRDefault'],
},
};
}

export function handleShapTextboxImport({ shape, params }) {
const schemaAttrs = {};
const schemaTextboxAttrs = {};
Expand Down Expand Up @@ -146,6 +242,42 @@ function buildStyles(styleObject) {
return style;
}

function buildVRectStyles(styleObject) {
let style = '';
for (const [prop, value] of Object.entries(styleObject)) {
style += `${prop}: ${value};`;
}

return style;
}

export function parsePointsToPixels(value) {
if (typeof value !== 'string') return value;

// Convert points to pixels (1pt ≈ 1.33px)
if (value.endsWith('pt')) {
const val = value.replace('pt', '');
if (isNaN(val)) {
return 0;
}
const points = parseFloat(val);
return Math.round(points * 1.33);
}

// Handle pixel values
if (value.endsWith('px')) {
const val = value.replace('px', '');
if (isNaN(val)) {
return 0;
}
return parseInt(val);
}

// Handle numeric values (assume pixels)
const numValue = parseFloat(value);
return isNaN(numValue) ? 0 : numValue;
}

export const pictNodeHandlerEntity = {
handlerName: 'handlePictNode',
handler: handlePictNode,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,13 @@ export const ContentBlock = Node.create({

addAttributes() {
return {
horizontalRule: {
default: false,
renderDOM: ({ horizontalRule }) => {
if (!horizontalRule) return {};
return { 'data-horizontal-rule': 'true' };
},
},
size: {
default: null,
renderDOM: ({ size }) => {
Expand All @@ -27,8 +34,9 @@ export const ContentBlock = Node.create({
let style = '';
if (size.top) style += `top: ${size.top}px; `;
if (size.left) style += `left: ${size.left}px; `;
if (size.width) style += `width: ${size.width}px; `;
if (size.height) style += `height: ${size.height}px; `;
if (size.width) style += `width: ${size.width.toString().endsWith('%') ? size.width : `${size.width}px`}; `;
if (size.height)
style += `height: ${size.height.toString().endsWith('%') ? size.height : `${size.height}px`}; `;
return { style };
},
},
Expand Down
Binary file not shown.
24 changes: 24 additions & 0 deletions packages/super-editor/src/tests/export/rectNodeExporter.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
import { expect, it, describe, beforeEach } from 'vitest';
import { getExportedResult } from './export-helpers/index';

describe('RectNodeExporter', async () => {
const fileName = 'vrect-node.docx';
const result = await getExportedResult(fileName);
const body = {};

beforeEach(() => {
Object.assign(
body,
result.elements?.find((el) => el.name === 'w:body'),
);
});

it('should export v:rect with all attributes', () => {
const rect = body.elements[3].elements[1].elements[0].elements[0];
expect(rect.attributes.id).toBe('_x0000_i1079');
// Reverts back to the word doc value of 0 width
expect(rect.attributes.style).toBe('width: 0;height: 1.5pt;');
expect(rect.attributes.fillcolor).toBe('#a0a0a0');
expect(rect.attributes['o:hr']).toBe('t');
});
});
Loading
Loading