diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/utils.js b/packages/super-editor/src/core/super-converter/v3/handlers/utils.js index 9023c34e11..fa6ebe0205 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/utils.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/utils.js @@ -113,6 +113,36 @@ export function createTrackChangesPropertyHandler(xmlName, sdName = null, extraA }; } +/** + * Parses a measurement value, handling ECMA-376 percentage strings. + * + * Per ECMA-376 §17.18.90 (ST_TblWidth): when type="pct" and value contains "%", + * it should be interpreted as a whole percentage point (e.g., "100%" = 100%). + * Otherwise, percentages are in fiftieths (5000 = 100%). + * + * @param {any} value The raw value from w:w attribute + * @param {string|undefined} type The type from w:type attribute + * @returns {number|undefined} The parsed value, converted to fiftieths if needed + */ +export const parseMeasurementValue = (value, type) => { + if (value == null) return undefined; + const strValue = String(value); + + // Per ECMA-376 §17.18.90: when type="pct" and value contains "%", + // interpret as whole percentage and convert to fiftieths format + if (type === 'pct' && strValue.includes('%')) { + const percent = parseFloat(strValue); + if (!isNaN(percent)) { + // Convert whole percentage to OOXML fiftieths (100% → 5000) + return Math.round(percent * 50); + } + } + + // Standard integer parsing for numeric values + const intValue = parseInt(strValue, 10); + return isNaN(intValue) ? undefined : intValue; +}; + /** * Helper to create property handlers for measurement attributes (CT_TblWidth => w:w and w:type) * @param {string} xmlName The XML attribute name (with namespace). @@ -124,12 +154,14 @@ export function createMeasurementPropertyHandler(xmlName, sdName = null) { return { xmlName, sdNodeOrKeyName: sdName, - attributes: [ - createAttributeHandler('w:w', 'value', parseInteger, integerToString), - createAttributeHandler('w:type'), - ], - encode: (_, encodedAttrs) => { - return encodedAttrs['value'] != null ? encodedAttrs : undefined; + attributes: [createAttributeHandler('w:w', 'value', (v) => v, integerToString), createAttributeHandler('w:type')], + encode: (params, encodedAttrs) => { + // Parse the value with type context for ECMA-376 percentage string handling + const rawValue = encodedAttrs['value']; + const type = encodedAttrs['type']; + const parsedValue = parseMeasurementValue(rawValue, type); + if (parsedValue == null) return undefined; + return { ...encodedAttrs, value: parsedValue }; }, decode: function ({ node }) { const decodedAttrs = this.decodeAttributes({ node: { ...node, attrs: node.attrs[sdName] || {} } }); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/tblW/tblW-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/tblW/tblW-translator.test.js index 9af7b6dae6..fbd878e570 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/tblW/tblW-translator.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/tblW/tblW-translator.test.js @@ -18,6 +18,43 @@ describe('w:tblW translator', () => { expect(result.value).toBe(150); }); + // SD-1633: ECMA-376 percentage string handling + it('converts percentage string "100%" with type="pct" to fiftieths (5000)', () => { + const result = translator.encode({ + nodes: [{ attributes: { 'w:w': '100%', 'w:type': 'pct' } }], + }); + expect(result).toEqual({ value: 5000, type: 'pct' }); + }); + + it('converts percentage string "50%" with type="pct" to fiftieths (2500)', () => { + const result = translator.encode({ + nodes: [{ attributes: { 'w:w': '50%', 'w:type': 'pct' } }], + }); + expect(result).toEqual({ value: 2500, type: 'pct' }); + }); + + it('handles decimal percentage strings like "62.5%"', () => { + const result = translator.encode({ + nodes: [{ attributes: { 'w:w': '62.5%', 'w:type': 'pct' } }], + }); + expect(result).toEqual({ value: 3125, type: 'pct' }); + }); + + it('does not convert percentage string when type is not "pct"', () => { + const result = translator.encode({ + nodes: [{ attributes: { 'w:w': '100%', 'w:type': 'dxa' } }], + }); + // parseInt("100%") = 100, kept as-is for non-pct types + expect(result).toEqual({ value: 100, type: 'dxa' }); + }); + + it('handles numeric fiftieths format (5000 = 100%) unchanged', () => { + const result = translator.encode({ + nodes: [{ attributes: { 'w:w': '5000', 'w:type': 'pct' } }], + }); + expect(result).toEqual({ value: 5000, type: 'pct' }); + }); + it('returns undefined if w:w is missing', () => { const result = translator.encode({ nodes: [{ attributes: { 'w:type': 'dxa' } }] }); expect(result).toBeUndefined(); diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/tcW/tcW-translator.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/tcW/tcW-translator.test.js index 6dc14bdf9b..90757a8673 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/tcW/tcW-translator.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/tcW/tcW-translator.test.js @@ -22,6 +22,21 @@ describe('w:tcW translator (tcW)', () => { const result = tcWTranslator.encode({ nodes: [{ attributes: { 'w:type': 'dxa' } }] }); expect(result).toBeUndefined(); }); + + // SD-1633: ECMA-376 percentage string handling for cell widths + it('converts percentage string "62%" with type="pct" to fiftieths (3100)', () => { + const result = tcWTranslator.encode({ + nodes: [{ attributes: { 'w:w': '62%', 'w:type': 'pct' } }], + }); + expect(result).toEqual({ value: 3100, type: 'pct' }); + }); + + it('converts percentage string "8%" with type="pct" to fiftieths (400)', () => { + const result = tcWTranslator.encode({ + nodes: [{ attributes: { 'w:w': '8%', 'w:type': 'pct' } }], + }); + expect(result).toEqual({ value: 400, type: 'pct' }); + }); }); describe('decode', () => {