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
2 changes: 1 addition & 1 deletion packages/layout-engine/measuring/dom/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2498,7 +2498,7 @@ async function measureTableBlock(block: TableBlock, constraints: MeasureConstrai
const totalWidth = columnWidths.reduce((a, b) => a + b, 0);
// Scale to effectiveTargetWidth (resolved percentage or explicit width)
// This handles both scaling down (too wide) and scaling up (percentage-based)
if (totalWidth !== effectiveTargetWidth && effectiveTargetWidth > 0) {
if (totalWidth > effectiveTargetWidth && effectiveTargetWidth > 0) {
const scale = effectiveTargetWidth / totalWidth;
Comment on lines 2498 to 2502

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Scale columns up for explicit/percentage table widths

The new condition only scales column widths when totalWidth > effectiveTargetWidth, so tables with explicit/percentage widths where the OOXML grid sums to less than the resolved target will no longer be expanded to the target width. This means a 100% (or explicit) table can render narrower than its specified width, despite the preceding comment saying scaling should handle “scaling up (percentage-based).” This is a regression from the previous behavior where totalWidth !== effectiveTargetWidth, and it breaks tables that rely on percentage width to stretch columns to the available width.

Useful? React with 👍 / 👎.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @harbournick! I did it intentionally in order to not to expand tables with layout=fixed and explicit width of columns.

columnWidths = columnWidths.map((w) => Math.max(1, Math.round(w * scale)));
// Normalize to exact target width (handle rounding errors)
Expand Down
24 changes: 23 additions & 1 deletion packages/layout-engine/pm-adapter/src/attributes/paragraph.ts
Original file line number Diff line number Diff line change
Expand Up @@ -951,6 +951,25 @@ const extractDropCapRunFromParagraph = (para: PMNode): DropCapRun | null => {
return dropCapRun;
};

/**
* Extract the (raw) text color from the first `run` inside a paragraph node.
*
* Reads `run.attrs.runProperties.color.val` (typically a hex string without a leading `#`).
*
* @param paragraph - ProseMirror paragraph node to inspect.
* @returns The raw color value, or `undefined` if no run/color is present. May be non-string if the node attrs are not
* shaped as expected.
*/
const extractColorFromRun = (paragraph: PMNode): string | unknown => {
if (!Array.isArray(paragraph?.content) || paragraph?.content?.length === 0) {
return undefined;
} else {
const firstRun = paragraph?.content?.find((item) => item.type === 'run');
const runPr = firstRun?.attrs?.runProperties;
return safeGetProperty(safeGetProperty(runPr, 'color'), 'val');
}
};

/**
* Compute Word paragraph layout for numbered paragraphs.
*
Expand Down Expand Up @@ -1036,7 +1055,10 @@ export const computeWordLayoutForParagraph = (
paragraphNode && converterContext ? hydrateMarkerStyleAttrs(paragraphNode, converterContext, resolvedPpr) : null;

if (markerHydration) {
const resolvedColor = markerHydration.color ? `#${markerHydration.color.replace('#', '')}` : undefined;
const rawColorFromPr = paragraphNode ? extractColorFromRun(paragraphNode) : undefined;
const resultColorFromPr = typeof rawColorFromPr === 'string' && rawColorFromPr ? `#${rawColorFromPr}` : undefined;
const resolvedColor = markerHydration.color ? `#${markerHydration.color.replace('#', '')}` : resultColorFromPr;

markerRun = {
fontFamily: markerHydration.fontFamily ?? 'Times New Roman',
fontSize: markerHydration.fontSize / 2, // half-points to points
Expand Down
49 changes: 49 additions & 0 deletions packages/super-editor/src/core/super-converter/helpers.js
Original file line number Diff line number Diff line change
Expand Up @@ -380,6 +380,54 @@ const rgbToHex = (rgb) => {
return '#' + rgb.match(/\d+/g).map(componentToHex).join('');
};

const DEFAULT_SHADING_FOREGROUND_COLOR = '#000000';

const hexToRgb = (hex) => {
const normalized = normalizeHexColor(hex);
if (!normalized) return null;
return {
r: Number.parseInt(normalized.slice(0, 2), 16),
g: Number.parseInt(normalized.slice(2, 4), 16),
b: Number.parseInt(normalized.slice(4, 6), 16),
};
};

const clamp01 = (value) => {
if (!Number.isFinite(value)) return 0;
return Math.min(1, Math.max(0, value));
};

const blendHexColors = (backgroundHex, foregroundHex, foregroundRatio) => {
const background = hexToRgb(backgroundHex);
const foreground = hexToRgb(foregroundHex);
if (!background || !foreground) return null;
const ratio = clamp01(foregroundRatio);

const r = Math.round(background.r * (1 - ratio) + foreground.r * ratio);
const g = Math.round(background.g * (1 - ratio) + foreground.g * ratio);
const b = Math.round(background.b * (1 - ratio) + foreground.b * ratio);

const toByte = (n) => n.toString(16).padStart(2, '0').toUpperCase();
return `${toByte(r)}${toByte(g)}${toByte(b)}`;
};

const resolveShadingFillColor = (shading) => {
if (!shading || typeof shading !== 'object') return null;

const fill = normalizeHexColor(shading.fill);
if (!fill) return null;

const val = typeof shading.val === 'string' ? shading.val.trim().toLowerCase() : '';
const pctMatch = val.match(/^pct(\d{1,3})$/);
if (!pctMatch) return fill;

const pct = Number.parseInt(pctMatch[1], 10);
if (!Number.isFinite(pct) || pct < 0 || pct > 100) return fill;

const foreground = normalizeHexColor(shading.color) ?? DEFAULT_SHADING_FOREGROUND_COLOR;
return blendHexColors(fill, foreground, pct / 100) ?? fill;
};

const getLineHeightValueString = (lineHeight, defaultUnit, lineRule = '', isObject = false) => {
let [value, unit] = parseSizeUnit(lineHeight);
if (Number.isNaN(value) || value === 0) return {};
Expand Down Expand Up @@ -519,4 +567,5 @@ export {
polygonUnitsToPixels,
pixelsToPolygonUnits,
convertSizeToCSS,
resolveShadingFillColor,
};
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,7 @@ const encode = (params, encodedAttrs) => {
extraParams: {
row,
table: node,
tableProperties: encodedAttrs.tableProperties,
tableBorders: encodedAttrs.borders,
tableLook,
columnWidths,
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { eighthPointsToPixels, twipsToPixels } from '@converter/helpers';
import { eighthPointsToPixels, twipsToPixels, resolveShadingFillColor } from '@converter/helpers';
import { translator as tcPrTranslator } from '../../tcPr';

/**
Expand All @@ -10,6 +10,7 @@ export function handleTableCellNode({
node,
table,
row,
tableProperties,
rowBorders,
baseTableBorders,
tableLook,
Expand Down Expand Up @@ -91,9 +92,10 @@ export function handleTableCellNode({
}

// Background
const background = {
color: tableCellProperties.shading?.fill,
};
const backgroundColor =
resolveShadingFillColor(tableCellProperties.shading) ?? resolveShadingFillColor(tableProperties?.shading);
const background = { color: backgroundColor };

// TODO: Do we need other background attrs?
if (background.color) attributes['background'] = background;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,7 +44,7 @@ describe('legacy-handle-table-cell-node', () => {
name: 'w:tcPr',
elements: [
{ name: 'w:tcW', attributes: { 'w:w': '1440', 'w:type': 'dxa' } }, // 1in => 96px
{ name: 'w:shd', attributes: { 'w:fill': '#ABCDEF' } },
{ name: 'w:shd', attributes: { 'w:fill': 'ABCDEF' } },
{ name: 'w:gridSpan', attributes: { 'w:val': '2' } },
{
name: 'w:tcMar',
Expand Down Expand Up @@ -138,7 +138,7 @@ describe('legacy-handle-table-cell-node', () => {
expect(out.attrs.widthType).toBe('dxa');

expect(out.attrs.colspan).toBe(2);
expect(out.attrs.background).toEqual({ color: '#ABCDEF' });
expect(out.attrs.background).toEqual({ color: 'ABCDEF' });
expect(out.attrs.verticalAlign).toBe('center');
expect(out.attrs.fontSize).toBe('12pt');
expect(out.attrs.fontFamily).toBe('Arial');
Expand All @@ -157,6 +157,40 @@ describe('legacy-handle-table-cell-node', () => {
expect(out.attrs.rowspan).toBe(3);
});

it('blends percentage table shading into a solid background color', () => {
const cellNode = { name: 'w:tc', elements: [{ name: 'w:p' }] };
const row = { name: 'w:tr', elements: [cellNode] };
const table = { name: 'w:tbl', elements: [row] };

const params = {
docx: {},
nodeListHandler: { handler: vi.fn(() => []) },
path: [],
editor: createEditorStub(),
};

const out = handleTableCellNode({
params,
node: cellNode,
table,
row,
tableProperties: {
shading: { val: 'pct50', color: '000000', fill: 'FFFFFF' },
},
rowBorders: {},
baseTableBorders: null,
columnIndex: 0,
columnWidth: null,
allColumnWidths: [90],
rowIndex: 0,
totalRows: 1,
totalColumns: 1,
_referencedStyles: null,
});

expect(out.attrs.background).toEqual({ color: '808080' });
});

it('applies firstRow/firstCol conditional borders from referenced styles', () => {
const cellNode = { name: 'w:tc', elements: [{ name: 'w:p' }] };
const row1 = { name: 'w:tr', elements: [cellNode] };
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ function encode(params, encodedAttrs) {
node,
table,
row,
tableProperties,
rowBorders,
baseTableBorders,
tableLook,
Expand All @@ -39,6 +40,7 @@ function encode(params, encodedAttrs) {
node,
table,
row,
tableProperties,
rowBorders,
baseTableBorders,
tableLook,
Expand Down