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
4 changes: 2 additions & 2 deletions packages/layout-engine/painters/dom/src/renderer.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2860,9 +2860,9 @@ export class DomPainter {
textDiv.style.display = 'flex';
textDiv.style.flexDirection = 'column';

// Use extracted vertical alignment or default to center
// Use extracted vertical alignment or default to top per OOXML spec
// In flex-direction: column, justifyContent controls vertical (main axis)
const verticalAlign = textVerticalAlign ?? 'center';
const verticalAlign = textVerticalAlign ?? 'top';
if (verticalAlign === 'top') {
textDiv.style.justifyContent = 'flex-start';
} else if (verticalAlign === 'bottom') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -182,7 +182,9 @@ export function extractBodyPrProperties(bodyPr) {
const bodyPrAttrs = bodyPr?.attributes || {};

// Extract vertical alignment from anchor attribute (t=top, ctr=center, b=bottom)
let verticalAlign = 'center'; // Default to center
// Per OOXML spec, when anchor is not specified, text box defaults to top alignment
// (confirmed by Word's VML fallback which shows v-text-anchor:top)
let verticalAlign = 'top'; // Default to top (OOXML spec default)
const anchorAttr = bodyPrAttrs['anchor'];
if (anchorAttr === 't') verticalAlign = 'top';
else if (anchorAttr === 'ctr') verticalAlign = 'center';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -360,16 +360,16 @@ describe('textbox-content-helpers', () => {
});

describe('extractBodyPrProperties', () => {
it('should return defaults for null input', () => {
it('should return defaults for null input (verticalAlign defaults to top per OOXML spec)', () => {
const result = extractBodyPrProperties(null);
expect(result.verticalAlign).toBe('center');
expect(result.verticalAlign).toBe('top');
expect(result.wrap).toBe('square');
expect(result.insets).toBeDefined();
});

it('should return defaults for empty bodyPr', () => {
it('should return defaults for empty bodyPr (verticalAlign defaults to top per OOXML spec)', () => {
const result = extractBodyPrProperties({});
expect(result.verticalAlign).toBe('center');
expect(result.verticalAlign).toBe('top');
expect(result.wrap).toBe('square');
});

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -127,7 +127,9 @@ export function extractStrokeColor(spPr, style) {

if (ln) {
const noFill = ln.elements?.find((el) => el.name === 'a:noFill');
if (noFill) return null;
if (noFill) {
return null;
}

const solidFill = ln.elements?.find((el) => el.name === 'a:solidFill');
if (solidFill) {
Expand Down Expand Up @@ -157,13 +159,29 @@ export function extractStrokeColor(spPr, style) {
}
}

if (!style) return '#000000';
// No stroke specified in spPr, check style reference
// Per ECMA-376: when no stroke is specified and no style exists, shape should have no stroke
if (!style) {
return null;
}

const lnRef = style.elements?.find((el) => el.name === 'a:lnRef');
if (!lnRef) return '#000000';
if (!lnRef) {
// No lnRef in style means no stroke specified - return null
return null;
}

// Per OOXML spec, lnRef idx="0" means "no stroke" - return null
const lnRefIdx = lnRef.attributes?.['idx'];
if (lnRefIdx === '0') {
return null;
}

const schemeClr = lnRef.elements?.find((el) => el.name === 'a:schemeClr');
if (!schemeClr) return '#000000';
if (!schemeClr) {
// No schemeClr in lnRef - return null rather than default black
return null;
}

const themeName = schemeClr.attributes?.['val'];
let color = getThemeColor(themeName);
Expand Down Expand Up @@ -193,7 +211,9 @@ export function extractStrokeColor(spPr, style) {
*/
export function extractFillColor(spPr, style) {
const noFill = spPr?.elements?.find((el) => el.name === 'a:noFill');
if (noFill) return null;
if (noFill) {
return null;
}

const solidFill = spPr?.elements?.find((el) => el.name === 'a:solidFill');
if (solidFill) {
Expand Down Expand Up @@ -252,17 +272,30 @@ export function extractFillColor(spPr, style) {
return '#cccccc'; // placeholder color for now
}

if (!style) return '#5b9bd5';
// No fill specified in spPr, check style reference
// Per ECMA-376: when no fill is specified and no style exists, shape should be transparent
if (!style) {
return null;
}

const fillRef = style.elements?.find((el) => el.name === 'a:fillRef');
if (!fillRef) return '#5b9bd5';
if (!fillRef) {
// No fillRef in style means no fill specified - return transparent
return null;
}

// Per OOXML spec, fillRef idx="0" means "no fill" - return null to indicate transparent
const fillRefIdx = fillRef.attributes?.['idx'];
if (fillRefIdx === '0') return null;

if (fillRefIdx === '0') {
return null;
}

const schemeClr = fillRef.elements?.find((el) => el.name === 'a:schemeClr');
if (!schemeClr) return '#5b9bd5';
if (!schemeClr) {
// No schemeClr in fillRef - return transparent rather than default blue
return null;
}

const themeName = schemeClr.attributes?.['val'];
let color = getThemeColor(themeName);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -139,8 +139,44 @@ describe('extractStrokeColor', () => {
expect(extractStrokeColor(spPr, style)).toBe('#5b9bd5');
});

it('returns default black when nothing found', () => {
expect(extractStrokeColor({ elements: [] }, null)).toBe('#000000');
it('returns null (no stroke) when no stroke in spPr and no style provided', () => {
// Per ECMA-376: when no stroke is specified and no style exists, shape should have no stroke
expect(extractStrokeColor({ elements: [] }, null)).toBeNull();
});

it('returns null (no stroke) when no stroke in spPr and style has no lnRef', () => {
const spPr = { elements: [] };
const style = { elements: [] };
expect(extractStrokeColor(spPr, style)).toBeNull();
});

it('returns null (no stroke) when lnRef idx is 0', () => {
// Per OOXML spec, lnRef idx="0" means "no stroke"
const spPr = { elements: [] };
const style = {
elements: [
{
name: 'a:lnRef',
attributes: { idx: '0' },
elements: [],
},
],
};
expect(extractStrokeColor(spPr, style)).toBeNull();
});

it('returns null (no stroke) when lnRef has no schemeClr', () => {
const spPr = { elements: [] };
const style = {
elements: [
{
name: 'a:lnRef',
attributes: { idx: '1' },
elements: [], // No schemeClr
},
],
};
expect(extractStrokeColor(spPr, style)).toBeNull();
});
});

Expand Down Expand Up @@ -213,7 +249,43 @@ describe('extractFillColor', () => {
expect(extractFillColor(spPr, style)).toBe('#70ad47');
});

it('returns default accent1 when nothing found', () => {
expect(extractFillColor({ elements: [] }, null)).toBe('#5b9bd5');
it('returns null (transparent) when no fill in spPr and no style provided', () => {
// Per ECMA-376: when no fill is specified and no style exists, shape should be transparent
expect(extractFillColor({ elements: [] }, null)).toBeNull();
});

it('returns null (transparent) when no fill in spPr and style has no fillRef', () => {
const spPr = { elements: [] };
const style = { elements: [] };
expect(extractFillColor(spPr, style)).toBeNull();
});

it('returns null (transparent) when fillRef idx is 0', () => {
// Per OOXML spec, fillRef idx="0" means "no fill"
const spPr = { elements: [] };
const style = {
elements: [
{
name: 'a:fillRef',
attributes: { idx: '0' },
elements: [],
},
],
};
expect(extractFillColor(spPr, style)).toBeNull();
});

it('returns null (transparent) when fillRef has no schemeClr', () => {
const spPr = { elements: [] };
const style = {
elements: [
{
name: 'a:fillRef',
attributes: { idx: '1' },
elements: [], // No schemeClr
},
],
};
expect(extractFillColor(spPr, style)).toBeNull();
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -43,15 +43,15 @@ export const VectorShape = Node.create({
},

fillColor: {
default: '#5b9bd5',
default: null,
renderDOM: (attrs) => {
if (!attrs.fillColor) return {};
return { 'data-fill-color': attrs.fillColor };
},
},

strokeColor: {
default: '#000000',
default: null,
renderDOM: (attrs) => {
if (!attrs.strokeColor) return {};
return { 'data-stroke-color': attrs.strokeColor };
Expand Down Expand Up @@ -143,7 +143,7 @@ export const VectorShape = Node.create({
},

textVerticalAlign: {
default: 'center',
default: 'top', // Per OOXML spec, text box defaults to top alignment
rendered: false,
},

Expand Down
Loading