diff --git a/packages/layout-engine/painters/dom/src/renderer.ts b/packages/layout-engine/painters/dom/src/renderer.ts index 013b44e3ab..88c42ffdad 100644 --- a/packages/layout-engine/painters/dom/src/renderer.ts +++ b/packages/layout-engine/painters/dom/src/renderer.ts @@ -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') { diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/textbox-content-helpers.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/textbox-content-helpers.js index cc93689d04..fd25a38ed9 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/textbox-content-helpers.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/textbox-content-helpers.js @@ -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'; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/textbox-content-helpers.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/textbox-content-helpers.test.js index 67eb1b4fe8..d432f8481b 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/textbox-content-helpers.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/textbox-content-helpers.test.js @@ -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'); }); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.js index a6edbed0eb..61d44924cf 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.js @@ -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) { @@ -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); @@ -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) { @@ -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); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.test.js index 06cfd29118..3f625bc752 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/wp/helpers/vector-shape-helpers.test.js @@ -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(); }); }); @@ -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(); }); }); diff --git a/packages/super-editor/src/extensions/vector-shape/vector-shape.js b/packages/super-editor/src/extensions/vector-shape/vector-shape.js index 10c577f7a2..72cbcea503 100644 --- a/packages/super-editor/src/extensions/vector-shape/vector-shape.js +++ b/packages/super-editor/src/extensions/vector-shape/vector-shape.js @@ -43,7 +43,7 @@ export const VectorShape = Node.create({ }, fillColor: { - default: '#5b9bd5', + default: null, renderDOM: (attrs) => { if (!attrs.fillColor) return {}; return { 'data-fill-color': attrs.fillColor }; @@ -51,7 +51,7 @@ export const VectorShape = Node.create({ }, strokeColor: { - default: '#000000', + default: null, renderDOM: (attrs) => { if (!attrs.strokeColor) return {}; return { 'data-stroke-color': attrs.strokeColor }; @@ -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, },