feat: the debug dump reports every table cell's vertical alignment

Per cell the attr beside the computed value, cell and row height, the gap above
and below the content, and each block's margins — plus the style sheet and the
spacing model, which is what decides whether the content fills the box.
This commit is contained in:
Steffen Becker committed 2026-08-13 23:39:37 +02:00
1 parent 06c5f2b12a
commit f7d9d38204
3 files changed
+104 -2

No files matched your search

+7 -1
View File
@@ -17,6 +17,7 @@
import { getPageBreakDebug } from './lib/editor/extensions/pageBreaks';
import { getColumnsFlowDebug } from './lib/editor/extensions/columnsFlow';
import { getTextBoxDebug } from './lib/editor/extensions/textBox';
import { getTableCellDebug } from './lib/editor/extensions/tableCellAlign';
import { getColorDebug } from './lib/utils/colorDebug';
import { resetHistoryLog } from './lib/utils/historyLog.svelte';
import { countText, type TextStats } from './lib/utils/wordCount';
@@ -653,7 +654,7 @@
try {
const json = editor.getJSON() as Parameters<typeof buildOdt>[0];
const { buildDocx } = await import('./lib/export/docx');
const bytes = await buildDocx(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, pageRtl, noteSettings());
const bytes = await buildDocx(json, pageMargins, pageOrientation, hfOpts(), odfFromLanguage(documentLanguage), pageFormat, styleSheet(), tabIntervalCm, spacingModel, pageRtl, noteSettings());
await saveAsDocx(bytes, suggestedFilenameDocx(json));
} catch (err) {
if ((err as DOMException)?.name === 'AbortError') return;
@@ -808,6 +809,11 @@
pageBreaks: snapshot,
columnsFlow: getColumnsFlowDebug(editor.view),
textBoxes: getTextBoxDebug(editor.view),
// Cell alignment reads as broken whenever the content fills the box, so the
// dump carries the spacing that decides that: the sheet and the model it uses.
tableCells: getTableCellDebug(editor.view),
spacingModel,
styles: styleSheet(),
colors: getColorDebug(editor),
};
const blob = new Blob([JSON.stringify(payload, null, 2)], { type: 'application/json' });
+1 -1
View File
@@ -67,7 +67,7 @@ One header and one footer (`HfDoc` = a single-paragraph TipTap doc per zone, per
## Debug tooling (dev only)
In dev builds a **Debug** button (`App.svelte`) downloads a JSON snapshot combining `getPageBreakDebug(view)` (leaves, placements, rendered spacers, table-break bands, live overlay geometry) and `getColorDebug(editor)` (selection marks, text runs, document colors, DOM spans) — used to diagnose pagination and color round-trip issues.
In dev builds a **Debug** button (`App.svelte`) downloads a JSON snapshot combining `getPageBreakDebug(view)` (leaves, placements, rendered spacers, table-break bands, live overlay geometry), `getTableCellDebug(view)` (per cell: the `verticalAlign` attr beside the computed value, cell/row height, the gap above and below the content, and every block's margins/`--space-before` — cell alignment reads as broken whenever the content fills the box, so the numbers that decide that travel with it), the style sheet + spacing model, and `getColorDebug(editor)` (selection marks, text runs, document colors, DOM spans).
## Per-section header/footer
@@ -1,4 +1,5 @@
import { Extension } from '@tiptap/core';
import type { EditorView } from '@tiptap/pm/view';
// Where a cell's content sits in its box: a `verticalAlign` attr on table cells,
// rendered as the cell's CSS vertical-align. null = top, which is what both
@@ -30,3 +31,98 @@ export const TableCellAlign = Extension.create({
];
},
});
// What the debug dump reports per cell. gapAbove/gapBelow measure against the cell's content
// box, so a block's own margin reads as a gap the alignment cannot take back.
export type CellBlockDebug = {
tag: string;
styleName: string | null;
height: number;
marginTop: number;
marginBottom: number;
paddingTop: number;
paddingBottom: number;
spaceBefore: string;
lineHeight: string;
};
export type TableCellDebugEntry = {
pos: number;
row: number;
col: number;
textPreview: string;
verticalAlign: CellVerticalAlign | null;
inlineStyle: string | null;
computedVerticalAlign: string;
cellHeight: number;
paddingTop: number;
paddingBottom: number;
gapAbove: number;
gapBelow: number;
rowHeightAttr: number | null;
rowHeight: number;
blocks: CellBlockDebug[];
};
const CELL_DEBUG_LIMIT = 120;
const px = (v: string) => Math.round(parseFloat(v) * 10) / 10 || 0;
const round1 = (v: number) => Math.round(v * 10) / 10;
export function getTableCellDebug(view: EditorView): TableCellDebugEntry[] {
const out: TableCellDebugEntry[] = [];
if (typeof getComputedStyle !== 'function') return out;
view.state.doc.descendants((node, pos) => {
if (node.type.name !== 'tableCell' && node.type.name !== 'tableHeader') return;
if (out.length >= CELL_DEBUG_LIMIT) return false;
const td = view.nodeDOM(pos);
if (!(td instanceof HTMLElement)) return false;
const tr = td.parentElement;
const cs = getComputedStyle(td);
const cell = td.getBoundingClientRect();
// The content box: what vertical-align distributes the free space in.
const top = cell.top + px(cs.borderTopWidth) + px(cs.paddingTop);
const bottom = cell.bottom - px(cs.borderBottomWidth) - px(cs.paddingBottom);
const blocks: CellBlockDebug[] = [];
let first: DOMRect | null = null;
let last: DOMRect | null = null;
for (const child of td.children) {
if (!(child instanceof HTMLElement) || child.dataset.pageBreakSpacer != null) continue;
const s = getComputedStyle(child);
// Resize handles and the like are absolutely positioned; only in-flow blocks count.
if (s.position === 'absolute') continue;
const r = child.getBoundingClientRect();
first ??= r;
last = r;
blocks.push({
tag: child.tagName,
styleName: child.dataset.style ?? null,
height: round1(r.height),
marginTop: px(s.marginTop),
marginBottom: px(s.marginBottom),
paddingTop: px(s.paddingTop),
paddingBottom: px(s.paddingBottom),
spaceBefore: s.getPropertyValue('--space-before').trim(),
lineHeight: s.lineHeight,
});
}
out.push({
pos,
row: tr?.parentElement ? [...tr.parentElement.children].indexOf(tr) : -1,
col: tr ? [...tr.children].indexOf(td) : -1,
textPreview: (node.textContent ?? '').slice(0, 40),
verticalAlign: (node.attrs.verticalAlign as CellVerticalAlign | null) ?? null,
inlineStyle: td.getAttribute('style'),
computedVerticalAlign: cs.verticalAlign,
cellHeight: round1(cell.height),
paddingTop: px(cs.paddingTop),
paddingBottom: px(cs.paddingBottom),
gapAbove: first ? round1(first.top - top) : 0,
gapBelow: last ? round1(bottom - last.bottom) : 0,
rowHeightAttr: (tr && view.state.doc.resolve(pos).parent.attrs.rowHeight as number | null) ?? null,
rowHeight: tr ? round1(tr.getBoundingClientRect().height) : 0,
blocks,
});
return false; // cells hold blocks, not other cells
});
return out;
}