Skip to content
Open
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 @@ -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).
Expand All @@ -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] || {} } });
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
Loading