fix: imports re-derive a registry table style's regions and unbake its text
Both importers resolve the table's style name + look + grid position through resolveTableCell: the region attr comes back on each cell, a bold region's runs convert like headings (boldByDefault) and a run matching the region's color loses that mark — the style's own text no longer lands as direct formatting on reopen. A DOCX file carrying its own w:tblStylePr keeps the baked areas instead; a style name not in the registry stays untouched.
This commit is contained in:
1 parent
c473fd5f0f
commit
4c55e9b9f5
2 files changed
+98
-25
No files matched your search
+32
-4
@@ -7,7 +7,7 @@ import { HEADER_SHADE } from '../editor/extensions/tableHeaderRow';
|
||||
import { fitInlineImage, framePx } from '../editor/extensions/image';
|
||||
import { formatTabStops } from '../editor/extensions/tabStops';
|
||||
import type { CapsMode, LineStyle } from '../editor/extensions/textEffects';
|
||||
import { tableLookAttr } from '../styles/tableStyles';
|
||||
import { builtinTableStyles, parseTableLook, resolveTableCell, tableLookAttr } from '../styles/tableStyles';
|
||||
import { formatOrdinal, orderedTypeFromFormat, orderedTypeAttrAt, childCycle, ROOT_ORDERED_CYCLE, type OrderedCycle } from '../utils/orderedListTypes';
|
||||
import { bulletCharAttr, bulletCharFromDocx } from '../utils/bulletListTypes';
|
||||
import { DATE_FORMATS, TIME_FORMATS, docxPicture, toDateValue } from '../utils/dateTime';
|
||||
@@ -2186,6 +2186,22 @@ function condPaint(areas: CondArea[]): { fill?: string; run: RunProps } {
|
||||
return { fill, run };
|
||||
}
|
||||
|
||||
// The export bakes a region's color onto every run (it is CSS in the editor); a run
|
||||
// matching the region's own color carries the style, not direct formatting. Bold rides
|
||||
// the boldByDefault channel instead, so an explicitly normal run keeps its override.
|
||||
function unbakeRegionColor(nodes: Node[], color: string): void {
|
||||
for (const n of nodes) {
|
||||
if (n.content) unbakeRegionColor(n.content, color);
|
||||
const ts = (n.marks ?? []).find((m) => m.type === 'textStyle');
|
||||
if (ts?.attrs?.color !== color) continue;
|
||||
delete ts.attrs.color;
|
||||
if (Object.keys(ts.attrs).length === 0) {
|
||||
n.marks = n.marks!.filter((m) => m !== ts);
|
||||
if (n.marks.length === 0) delete n.marks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// A conditional area's bold/italic/colour as real marks on the cell's text, skipping what
|
||||
// a run already declares — the file's own formatting outranks its table style.
|
||||
function bakeCellRuns(nodes: Node[], props: RunProps): void {
|
||||
@@ -2269,6 +2285,12 @@ function buildTable(tbl: Element, ctx: Ctx): Node | null {
|
||||
const conds = ctx.styles.tableConditional(styleId);
|
||||
const flags = docxLookFlags(fc(tbl, 'tblPr'));
|
||||
const band = ctx.styles.tableBandSize(styleId);
|
||||
// A registry style instead is name-only (our own export): its regions are re-derived
|
||||
// per cell from name + look + grid position. A file carrying w:tblStylePr wins above.
|
||||
const named = ctx.styles.tableStyleName(styleId);
|
||||
const look = docxTableLook(fc(tbl, 'tblPr'));
|
||||
const regStyle = !conds.size && named ? builtinTableStyles()[named] : undefined;
|
||||
const regLook = parseTableLook(look);
|
||||
const gridCols = useWeights?.length
|
||||
?? Math.max(1, ...trs.map((tr) => fcAll(tr, 'tc').reduce((n, tc) => n + (intAttr(fc(fc(tc, 'tcPr'), 'gridSpan'), W, 'val') ?? 1), 0)));
|
||||
for (let ri = 0; ri < trs.length; ri++) {
|
||||
@@ -2290,11 +2312,19 @@ function buildTable(tbl: Element, ctx: Ctx): Node | null {
|
||||
const box: GridBox = { row: ri, col, rowEnd, colEnd: col + colspan };
|
||||
const areas = conds.size ? condAreasFor(conds, flags, band, box, trs.length, gridCols) : [];
|
||||
const paint = condPaint(areas);
|
||||
const styled = regStyle ? resolveTableCell(regStyle, {
|
||||
row: ri, col, rowSpan: rowEnd - ri, colSpan: colspan, rows: trs.length, cols: gridCols,
|
||||
}, regLook) : null;
|
||||
const fill = fc(tcPr, 'shd')?.getAttributeNS(W, 'fill');
|
||||
const bg = fill ? hexColor(fill) : paint.fill;
|
||||
const blocks = convertBlocks(Array.from(tc.children), ctx, 'cell', bg === HEADER_SHADE);
|
||||
// Header-shaded cells and a bold region render bold by default (CSS); a baked-bold
|
||||
// run needs no mark, only an explicitly normal one gets fontWeight:normal.
|
||||
const blocks = convertBlocks(Array.from(tc.children), ctx, 'cell',
|
||||
bg === HEADER_SHADE || styled?.text.bold === true);
|
||||
bakeCellRuns(blocks, paint.run);
|
||||
if (styled?.text.color) unbakeRegionColor(blocks, styled.text.color);
|
||||
const attrs: Record<string, unknown> = { colspan, rowspan: 1 };
|
||||
if (styled?.regions.length) attrs.region = styled.regions.join(' ');
|
||||
if (bg) attrs.backgroundColor = bg;
|
||||
// w:vAlign — Word's "center" is the editor's "middle"; "top"/"both" stay the default.
|
||||
const vAlignEl = fc(tcPr, 'vAlign');
|
||||
@@ -2349,7 +2379,6 @@ function buildTable(tbl: Element, ctx: Ctx): Node | null {
|
||||
rows.push(row);
|
||||
}
|
||||
if (rows.length === 0) return null;
|
||||
const named = ctx.styles.tableStyleName(styleId);
|
||||
const attrs: Record<string, unknown> = { ...(tableMargins(tbl, useWeights, ctx, padBase[3]) ?? {}) };
|
||||
if (pad) attrs.cellPadding = pad;
|
||||
// w:tblHeader on the first row: Word repeats it at the top of every page the table
|
||||
@@ -2360,7 +2389,6 @@ function buildTable(tbl: Element, ctx: Ctx): Node | null {
|
||||
if (named) {
|
||||
attrs.tableStyle = named;
|
||||
// '' is a declared all-off look — dropping it would revert to the default look.
|
||||
const look = docxTableLook(fc(tbl, 'tblPr'));
|
||||
if (look != null) attrs.tableLook = look;
|
||||
}
|
||||
return Object.keys(attrs).length ? { type: 'table', attrs, content: rows } : { type: 'table', content: rows };
|
||||
|
||||
+66
-21
@@ -7,7 +7,10 @@ import { fitInlineImage, framePx } from '../editor/extensions/image';
|
||||
import { odfChartDataUrl } from './chart';
|
||||
import { formatTabStops, normalizeLeader } from '../editor/extensions/tabStops';
|
||||
import type { CapsMode, LineStyle } from '../editor/extensions/textEffects';
|
||||
import { TABLE_REGIONS, tableLookAttr, type TableLook, type TableRegion } from '../styles/tableStyles';
|
||||
import {
|
||||
TABLE_REGIONS, builtinTableStyles, parseTableLook, resolveTableCell, tableLookAttr,
|
||||
type TableLook, type TableRegion,
|
||||
} from '../styles/tableStyles';
|
||||
import { orderedTypeFromFormat, orderedTypeAttrAt, childCycle, ROOT_ORDERED_CYCLE, type OrderedCycle } from '../utils/orderedListTypes';
|
||||
import { bulletCharAttr, bulletCharFromOdf } from '../utils/bulletListTypes';
|
||||
import { matchFormat, toDateValue, type Token } from '../utils/dateTime';
|
||||
@@ -2561,9 +2564,58 @@ const FORMAT_KINDS: Record<string, FormatKind | undefined> = {
|
||||
'currency-style': 'currency', 'date-style': 'date',
|
||||
};
|
||||
|
||||
// The export bakes a region's color onto every run (it is CSS in the editor); a run
|
||||
// matching the region's own color carries the style, not direct formatting. Bold rides
|
||||
// the boldByDefault channel instead, so an explicitly normal run keeps its override.
|
||||
function unbakeRegionColor(nodes: Node[], color: string): void {
|
||||
for (const n of nodes) {
|
||||
if (n.content) unbakeRegionColor(n.content, color);
|
||||
const ts = (n.marks ?? []).find((m) => m.type === 'textStyle');
|
||||
if (ts?.attrs?.color !== color) continue;
|
||||
delete ts.attrs.color;
|
||||
if (Object.keys(ts.attrs).length === 0) {
|
||||
n.marks = n.marks!.filter((m) => m !== ts);
|
||||
if (n.marks.length === 0) delete n.marks;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function convertTable(el: Element, ctx: Ctx): Node | null {
|
||||
const weights = columnWeights(el, ctx.resolver);
|
||||
|
||||
// The named table style behind the automatic one, hoisted: a registry style's regions
|
||||
// are re-derived per cell from name + look + grid position, as the editor paints them.
|
||||
const named = ctx.resolver.namedAncestor(el.getAttributeNS(NS.table, 'style-name'), 'table');
|
||||
const styleName = named ? displayStyleName(named) : null;
|
||||
const look = odfTableLook(el);
|
||||
const regStyle = styleName ? builtinTableStyles()[styleName] : undefined;
|
||||
const regLook = parseTableLook(look);
|
||||
|
||||
const rowEls: { el: Element; header: boolean }[] = [];
|
||||
for (const child of Array.from(el.children)) {
|
||||
if (child.namespaceURI !== NS.table) continue;
|
||||
if (child.localName === 'table-row') rowEls.push({ el: child, header: false });
|
||||
else if (child.localName === 'table-header-rows' || child.localName === 'table-rows') {
|
||||
const header = child.localName === 'table-header-rows';
|
||||
for (const rowEl of Array.from(child.children)) {
|
||||
if (rowEl.namespaceURI === NS.table && rowEl.localName === 'table-row') rowEls.push({ el: rowEl, header });
|
||||
}
|
||||
}
|
||||
}
|
||||
// Grid extent up front — resolveTableCell needs the totals (lastRow, banding).
|
||||
const gridRows = rowEls.length;
|
||||
let gridCols = weights?.length ?? 0;
|
||||
if (!gridCols && rowEls[0]) {
|
||||
for (const c of Array.from(rowEls[0].el.children)) {
|
||||
if (c.namespaceURI !== NS.table) continue;
|
||||
const rep = Math.min(256, parseInt(c.getAttributeNS(NS.table, 'number-columns-repeated') ?? '1', 10) || 1);
|
||||
if (c.localName === 'covered-table-cell') gridCols += rep;
|
||||
else if (c.localName === 'table-cell') {
|
||||
gridCols += rep * (parseInt(c.getAttributeNS(NS.table, 'number-columns-spanned') ?? '1', 10) || 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
const rows: Node[] = [];
|
||||
// Source-row bookkeeping: a row of only covered cells is dropped (the editor cannot
|
||||
// hold a cell-less row), so every span reaching over it must shrink by one.
|
||||
@@ -2606,9 +2658,15 @@ function convertTable(el: Element, ctx: Ctx): Node | null {
|
||||
const v = borderAttrFromOdf(rawBorders[side]);
|
||||
if (v !== null) borders[attr] = v;
|
||||
}
|
||||
// Header-shaded cells render bold by default (CSS); convert their runs like headings
|
||||
// so a baked-bold run needs no mark and only an explicitly normal run gets one.
|
||||
const blocks = convertBlocks(Array.from(cellEl.children), ctx, 'cell', backgroundColor === HEADER_SHADE);
|
||||
// Header-shaded cells and a bold region render bold by default (CSS); convert their
|
||||
// runs like headings, so a baked-bold run needs no mark and only a normal run gets one.
|
||||
const paint = regStyle ? resolveTableCell(regStyle, {
|
||||
row: rowIdx, col: colIndex, rowSpan: rowspan, colSpan: colspan,
|
||||
rows: gridRows, cols: gridCols,
|
||||
}, regLook) : null;
|
||||
const blocks = convertBlocks(Array.from(cellEl.children), ctx, 'cell',
|
||||
backgroundColor === HEADER_SHADE || paint?.text.bold === true);
|
||||
if (paint?.text.color) unbakeRegionColor(blocks, paint.text.color);
|
||||
// LibreOffice writes the formula in its own language behind an `ooow:` prefix;
|
||||
// the cached result is the cell's text and needs nothing here.
|
||||
const rawFormula = cellEl.getAttributeNS(NS.table, 'formula');
|
||||
@@ -2617,6 +2675,7 @@ function convertTable(el: Element, ctx: Ctx): Node | null {
|
||||
const cellFormat = formula ? cellNumberFormat(cellEl, ctx) : null;
|
||||
for (let r = 0; r < repeated; r++) {
|
||||
const attrs: Record<string, unknown> = { colspan, rowspan, ...borders };
|
||||
if (paint?.regions.length) attrs.region = paint.regions.join(' ');
|
||||
if (rowspan > 1) placed.push({ attrs, row: rowIdx, span: rowspan });
|
||||
if (formula) attrs.formula = formula;
|
||||
if (cellFormat) attrs.cellFormat = cellFormat;
|
||||
@@ -2640,26 +2699,13 @@ function convertTable(el: Element, ctx: Ctx): Node | null {
|
||||
rows.push(row);
|
||||
};
|
||||
|
||||
for (const child of Array.from(el.children)) {
|
||||
if (child.namespaceURI !== NS.table) continue;
|
||||
if (child.localName === 'table-row') addRow(child, false);
|
||||
else if (child.localName === 'table-header-rows' || child.localName === 'table-rows') {
|
||||
const header = child.localName === 'table-header-rows';
|
||||
for (const rowEl of Array.from(child.children)) {
|
||||
if (rowEl.namespaceURI === NS.table && rowEl.localName === 'table-row') addRow(rowEl, header);
|
||||
}
|
||||
}
|
||||
}
|
||||
for (const { el: rowEl, header } of rowEls) addRow(rowEl, header);
|
||||
|
||||
if (rows.length === 0) return null;
|
||||
if (dropped.length) for (const p of placed) {
|
||||
const cut = dropped.filter((d) => d > p.row && d < p.row + p.span).length;
|
||||
if (cut) p.attrs.rowspan = p.span - cut;
|
||||
}
|
||||
// The named table style behind the automatic one. ODF stores no banding, so only the
|
||||
// name comes back — the look rides on the cell attrs above, and the editor re-derives
|
||||
// the regions from the registry (refreshTableStyles).
|
||||
const named = ctx.resolver.namedAncestor(el.getAttributeNS(NS.table, 'style-name'), 'table');
|
||||
const attrs: Record<string, unknown> = { ...(tableMargins(el, ctx) ?? {}), ...tableSpacing(el, ctx) };
|
||||
if (cellPad) attrs.cellPadding = cellPad;
|
||||
// The default is to allow a break, so only the explicit "no" is worth an attr.
|
||||
@@ -2671,12 +2717,11 @@ function convertTable(el: Element, ctx: Ctx): Node | null {
|
||||
if (Array.from(el.children).some(c => c.namespaceURI === NS.table && c.localName === 'table-header-rows')) {
|
||||
attrs.repeatHeader = true;
|
||||
}
|
||||
if (named) {
|
||||
attrs.tableStyle = displayStyleName(named);
|
||||
if (styleName) {
|
||||
attrs.tableStyle = styleName;
|
||||
// Which conditional areas the table opts into (ODF's table template attributes).
|
||||
// Absent ⇒ leave the attr null, so parseTableLook falls back to the default.
|
||||
// '' is a declared all-off look — dropping it would revert to the default look.
|
||||
const look = odfTableLook(el);
|
||||
if (look != null) attrs.tableLook = look;
|
||||
}
|
||||
return Object.keys(attrs).length ? { type: 'table', attrs, content: rows } : { type: 'table', content: rows };
|
||||
|
||||
Reference in new issue
Block a user