feat: a table keeps the space its style puts above and below it
ODF puts fo:margin-top/-bottom on the table style and LibreOffice honours them like a paragraph's; we dropped both, so every callout table in a fixture sat half a line too tight and the document ran a page short. With them the Math Guide paginates to LibreOffice's 63 pages exactly.
This commit is contained in:
1 parent
0712d44ebf
commit
e2179b75fb
5 files changed
+55
-10
No files matched your search
@@ -89,6 +89,12 @@ We deliberately keep TipTap's `resizable: false` (so its own columnResizing plug
|
||||
without it an emphasis-only area such as first/last column or the total row is **invisible** in
|
||||
a tile, since the tiles hold no real text — only fills and borders would show.
|
||||
|
||||
- **A table's own space above/below** (`marginTop`/`marginBottom`, cm, beside the two horizontal
|
||||
ones) — ODF `fo:margin-top`/`-bottom` on the table style, which LibreOffice honours like a
|
||||
paragraph's. Rendered on the `.tableWrapper` (`TableView`, plus the inline style for the static
|
||||
paths). Word has no table-level spacing, so it round-trips through ODF only. the Math Guide's callout
|
||||
tables carry 0.109/0.409cm and lost half a line each without it.
|
||||
|
||||
## Cell metrics measured against LibreOffice
|
||||
|
||||
- **No space above a cell's first block.** LibreOffice adds none there (its
|
||||
|
||||
@@ -44,8 +44,13 @@ function marginCm(value: string): number {
|
||||
function tableMarginStyle(attrs: Record<string, unknown>): Record<string, string> {
|
||||
const ml = Number(attrs.marginLeft) || 0;
|
||||
const mr = Number(attrs.marginRight) || 0;
|
||||
if (!ml && !mr) return {};
|
||||
return { style: `margin-left: ${ml}cm; margin-right: ${mr}cm; width: calc(100% - ${round3(ml + mr)}cm)` };
|
||||
const mt = Number(attrs.marginTop) || 0;
|
||||
const mb = Number(attrs.marginBottom) || 0;
|
||||
const decls: string[] = [];
|
||||
if (ml || mr) decls.push(`margin-left: ${ml}cm`, `margin-right: ${mr}cm`, `width: calc(100% - ${round3(ml + mr)}cm)`);
|
||||
if (mt) decls.push(`margin-top: ${mt}cm`);
|
||||
if (mb) decls.push(`margin-bottom: ${mb}cm`);
|
||||
return decls.length ? { style: decls.join('; ') } : {};
|
||||
}
|
||||
|
||||
export const tableColumnResizeKey = new PluginKey<ResizeState>('tableColumnResize');
|
||||
@@ -373,6 +378,18 @@ export const TableColumnResize = Extension.create({
|
||||
parseHTML: (element: HTMLElement) => marginCm(element.style.marginRight),
|
||||
renderHTML: () => ({}),
|
||||
},
|
||||
// The space above/below the table (ODF fo:margin-top/-bottom on the table
|
||||
// style). Word has no equivalent, so it only round-trips through ODF.
|
||||
marginTop: {
|
||||
default: 0,
|
||||
parseHTML: (element: HTMLElement) => marginCm(element.style.marginTop),
|
||||
renderHTML: () => ({}),
|
||||
},
|
||||
marginBottom: {
|
||||
default: 0,
|
||||
parseHTML: (element: HTMLElement) => marginCm(element.style.marginBottom),
|
||||
renderHTML: () => ({}),
|
||||
},
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
@@ -42,8 +42,12 @@ export function columnPercents(weights: (number | null)[]): number[] {
|
||||
function applyMargins(node: PMNode, wrapper: HTMLElement): void {
|
||||
const ml = (node.attrs.marginLeft as number) || 0;
|
||||
const mr = (node.attrs.marginRight as number) || 0;
|
||||
const mt = (node.attrs.marginTop as number) || 0;
|
||||
const mb = (node.attrs.marginBottom as number) || 0;
|
||||
wrapper.style.marginLeft = ml ? `${ml}cm` : '';
|
||||
wrapper.style.marginRight = mr ? `${mr}cm` : '';
|
||||
wrapper.style.marginTop = mt ? `${mt}cm` : '';
|
||||
wrapper.style.marginBottom = mb ? `${mb}cm` : '';
|
||||
}
|
||||
|
||||
// The node view never calls the node's renderHTML, so the table-style hook the generated
|
||||
|
||||
+12
-7
@@ -808,7 +808,7 @@ type CellBlock =
|
||||
| { kind: 'heading'; level: number; style: ParaStyle }
|
||||
| CellListBlock;
|
||||
// A table's own margins in cm (0/0 tables are recorded as null).
|
||||
type TableMargins = { ml: number; mr: number };
|
||||
type TableMargins = { ml: number; mr: number; mt: number; mb: number };
|
||||
|
||||
// Collect the alignment + paragraph spacing of each listItem's first paragraph,
|
||||
// in DFS order — matching the order that odf-kit emits <text:list-item> elements
|
||||
@@ -930,9 +930,12 @@ function applyTableMargins(odtBytes: Uint8Array, margins: (TableMargins | null)[
|
||||
margins.forEach((m, i) => {
|
||||
if (!m) return;
|
||||
const width = Math.round((contentWidthCm - m.ml - m.mr) * 1000) / 1000;
|
||||
const horiz = m.ml || m.mr
|
||||
? ` style:width="${width}cm" fo:margin-left="${m.ml}cm" fo:margin-right="${m.mr}cm"` : '';
|
||||
const vert = `${m.mt ? ` fo:margin-top="${m.mt}cm"` : ''}${m.mb ? ` fo:margin-bottom="${m.mb}cm"` : ''}`;
|
||||
content = content.replace(
|
||||
new RegExp(`(<style:style[^>]*style:name="Table${i + 1}"[^>]*>\\s*<style:table-properties)`),
|
||||
`$1 style:width="${width}cm" fo:margin-left="${m.ml}cm" fo:margin-right="${m.mr}cm"`,
|
||||
`$1${horiz}${vert}`,
|
||||
);
|
||||
});
|
||||
|
||||
@@ -2636,11 +2639,13 @@ function tableColumnWidthsCm(node: TiptapNode, contentWidthCm: number): string[]
|
||||
// text width. null = the table spans the full text width (the common case).
|
||||
function tableMarginsCm(node: TiptapNode, contentWidthCm: number): TableMargins | null {
|
||||
const round3 = (v: number) => Math.round(v * 1000) / 1000;
|
||||
const ml = Math.max(0, Number(node.attrs?.marginLeft) || 0);
|
||||
const mr = Math.max(0, Number(node.attrs?.marginRight) || 0);
|
||||
if (!ml && !mr) return null;
|
||||
if (ml + mr > contentWidthCm - 1) return null;
|
||||
return { ml: round3(ml), mr: round3(mr) };
|
||||
let ml = Math.max(0, Number(node.attrs?.marginLeft) || 0);
|
||||
let mr = Math.max(0, Number(node.attrs?.marginRight) || 0);
|
||||
const mt = Math.max(0, Number(node.attrs?.marginTop) || 0);
|
||||
const mb = Math.max(0, Number(node.attrs?.marginBottom) || 0);
|
||||
if (ml + mr > contentWidthCm - 1) ml = mr = 0;
|
||||
if (!ml && !mr && !mt && !mb) return null;
|
||||
return { ml: round3(ml), mr: round3(mr), mt: round3(mt), mb: round3(mb) };
|
||||
}
|
||||
|
||||
// Build an ODF table from a CUST_TABLE node, bypassing odf-kit's native walkTable to
|
||||
|
||||
+14
-1
@@ -1995,7 +1995,7 @@ function convertTable(el: Element, ctx: Ctx): Node | null {
|
||||
// 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) ?? {}) };
|
||||
const attrs: Record<string, unknown> = { ...(tableMargins(el, ctx) ?? {}), ...tableSpacing(el, ctx) };
|
||||
if (cellPad) attrs.cellPadding = cellPad;
|
||||
if (named) {
|
||||
attrs.tableStyle = displayStyleName(named);
|
||||
@@ -2051,6 +2051,19 @@ function tableMargins(el: Element, ctx: Ctx): { marginLeft: number; marginRight:
|
||||
return { marginLeft: round2(left), marginRight: round2(right) };
|
||||
}
|
||||
|
||||
// The space a table's own style puts above and below it (fo:margin-top/-bottom on
|
||||
// the table, which LibreOffice honours like a paragraph's).
|
||||
function tableSpacing(el: Element, ctx: Ctx): { marginTop?: number; marginBottom?: number } {
|
||||
const props = ctx.resolver.tableProps(el.getAttributeNS(NS.table, 'style-name'));
|
||||
const round3 = (v: number) => Math.round(v * 1000) / 1000;
|
||||
const out: { marginTop?: number; marginBottom?: number } = {};
|
||||
const top = lengthToCm(props['fo:margin-top']);
|
||||
const bottom = lengthToCm(props['fo:margin-bottom']);
|
||||
if (top && top > 0) out.marginTop = round3(top);
|
||||
if (bottom && bottom > 0) out.marginBottom = round3(bottom);
|
||||
return out;
|
||||
}
|
||||
|
||||
// Per-column proportional weights for the colwidth cell attr (tableView.ts uses
|
||||
// ratios only). Prefers LibreOffice's relative widths (rel-column-width) and
|
||||
// falls back to absolute cm. null when the document declares no usable widths.
|
||||
|
||||
Reference in new issue
Block a user