diff --git a/packages/layout-engine/measuring/dom/src/index.ts b/packages/layout-engine/measuring/dom/src/index.ts index a954b2b43..e6d52d30d 100644 --- a/packages/layout-engine/measuring/dom/src/index.ts +++ b/packages/layout-engine/measuring/dom/src/index.ts @@ -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; columnWidths = columnWidths.map((w) => Math.max(1, Math.round(w * scale))); // Normalize to exact target width (handle rounding errors) diff --git a/packages/layout-engine/pm-adapter/src/attributes/paragraph.ts b/packages/layout-engine/pm-adapter/src/attributes/paragraph.ts index 36bf00860..acf80a1a7 100644 --- a/packages/layout-engine/pm-adapter/src/attributes/paragraph.ts +++ b/packages/layout-engine/pm-adapter/src/attributes/paragraph.ts @@ -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. * @@ -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 diff --git a/packages/super-editor/src/core/super-converter/helpers.js b/packages/super-editor/src/core/super-converter/helpers.js index 2b296709c..910b13433 100644 --- a/packages/super-editor/src/core/super-converter/helpers.js +++ b/packages/super-editor/src/core/super-converter/helpers.js @@ -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 {}; @@ -519,4 +567,5 @@ export { polygonUnitsToPixels, pixelsToPolygonUnits, convertSizeToCSS, + resolveShadingFillColor, }; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/tbl/tbl-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/tbl/tbl-translator.js index 87f546874..21828751c 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/tbl/tbl-translator.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/tbl/tbl-translator.js @@ -127,6 +127,7 @@ const encode = (params, encodedAttrs) => { extraParams: { row, table: node, + tableProperties: encodedAttrs.tableProperties, tableBorders: encodedAttrs.borders, tableLook, columnWidths, diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.js index 233b40078..5cde95355 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.js @@ -1,4 +1,4 @@ -import { eighthPointsToPixels, twipsToPixels } from '@converter/helpers'; +import { eighthPointsToPixels, twipsToPixels, resolveShadingFillColor } from '@converter/helpers'; import { translator as tcPrTranslator } from '../../tcPr'; /** @@ -10,6 +10,7 @@ export function handleTableCellNode({ node, table, row, + tableProperties, rowBorders, baseTableBorders, tableLook, @@ -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; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.test.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.test.js index a6bf9b93e..0ea8c459b 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.test.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/helpers/legacy-handle-table-cell-node.test.js @@ -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', @@ -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'); @@ -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] }; diff --git a/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/tc-translator.js b/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/tc-translator.js index 1736be131..92ef85852 100644 --- a/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/tc-translator.js +++ b/packages/super-editor/src/core/super-converter/v3/handlers/w/tc/tc-translator.js @@ -21,6 +21,7 @@ function encode(params, encodedAttrs) { node, table, row, + tableProperties, rowBorders, baseTableBorders, tableLook, @@ -39,6 +40,7 @@ function encode(params, encodedAttrs) { node, table, row, + tableProperties, rowBorders, baseTableBorders, tableLook,