feat: the debug dump reports the colour actually painted

The colour section only read the marks, so a colour no mark carries —
a decoration's, a stylesheet rule's, the page's own — was invisible in
the dump. `painted` adds the computed colour per rendered run with the
element that declares it, the page colour variables read at the editor's
own element, and the document stylesheet's colour rules.
This commit is contained in:
Steffen Becker committed 2026-08-15 16:42:34 +02:00
1 parent c771193c83
commit 4e343f2014
2 files changed
+93 -1

No files matched your search

+1 -1
View File
@@ -141,7 +141,7 @@ ceiling noted in the file.
## 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), `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), `getFrameDebug(view)` (`caption.ts` — per picture: its attrs, its rendered span in the text column and its caption's; every other section measures the flow downwards, and a misplaced caption is a sideways question), the style sheet + spacing model, and `getColorDebug(editor)` (selection marks, text runs, document colors, DOM spans).
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), `getFrameDebug(view)` (`caption.ts` — per picture: its attrs, its rendered span in the text column and its caption's; every other section measures the flow downwards, and a misplaced caption is a sideways question), the style sheet + spacing model, and `getColorDebug(editor)` (selection marks, text runs, document colors, DOM spans, and `painted`: the colour the browser actually computes per rendered run, the element that declares it, the page's colour variables and the document stylesheet's colour rules — a mark says what was asked for, and a decoration paints a colour no mark carries).
## Per-section header/footer
+92
View File
@@ -30,6 +30,23 @@ export type ColorDebugSnapshot = {
total: number;
sample: Array<{ dataColor: string | null; inlineStyle: string | null; text: string }>;
};
/** What the browser paints — the marks above only say what it was asked to. */
painted: {
/** The custom properties a run of no colour of its own resolves through. */
vars: Record<string, string>;
/** One entry per colour actually rendered, with where that colour comes from. */
colors: Record<string, { chars: number; sample: string; block: string; source: PaintSource | null }>;
/** The colour-carrying rules of the document stylesheet (styles/styleSheet.ts). */
styleRules: string[];
};
};
/** The nearest element declaring a colour, i.e. the channel a painted colour arrives on. */
type PaintSource = {
tag: string;
className: string;
dataColor: string | null;
inlineColor: string | null;
};
const TEXT_PREVIEW_LIMIT = 80;
@@ -55,6 +72,67 @@ function getTextStyleAttr(marks: readonly Mark[], attr: string): string | null {
return typeof v === 'string' && v ? v : null;
}
const PAINT_VARS = ['--color-page-text', '--color-page-bg', '--color-text', '--font-color'];
const INLINE_COLOR = /(?:^|;)\s*color\s*:\s*([^;]+)/;
const STYLE_RULE_LIMIT = 40;
// Walking up to the editor root: the first element declaring a colour is the channel the
// run is painted through. None of them = the page's own colour (editor.css).
function paintSource(from: HTMLElement, root: HTMLElement): PaintSource | null {
for (let e: HTMLElement | null = from; e; e = e.parentElement) {
const inline = INLINE_COLOR.exec(e.getAttribute('style') ?? '');
if (inline || e.hasAttribute('data-color')) {
return {
tag: e.tagName.toLowerCase(),
className: e.getAttribute('class') ?? '',
dataColor: e.getAttribute('data-color'),
inlineColor: inline ? inline[1].trim() : null,
};
}
if (e === root) break;
}
return null;
}
// The block a run sits in, named as the CSS would address it: its tag and the named
// paragraph style whose rule reaches it.
function blockOf(from: HTMLElement, root: HTMLElement): string {
const block = from.closest('p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10, li, td, th') ?? from;
const styled = from.closest('[data-style]');
const name = styled && root.contains(styled) ? styled.getAttribute('data-style') : null;
return name ? `${block.tagName.toLowerCase()}[${name}]` : block.tagName.toLowerCase();
}
function paintedColors(root: HTMLElement): ColorDebugSnapshot['painted']['colors'] {
const out: ColorDebugSnapshot['painted']['colors'] = {};
if (typeof document === 'undefined' || typeof getComputedStyle !== 'function') return out;
const walker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT);
for (let n = walker.nextNode(); n; n = walker.nextNode()) {
const text = n.textContent ?? '';
const el = n.parentElement;
if (!text.trim() || !el) continue;
const color = getComputedStyle(el).color;
if (!out[color]) {
out[color] = { chars: 0, sample: truncate(text.trim(), 40), block: blockOf(el, root), source: paintSource(el, root) };
}
out[color].chars += text.length;
}
return out;
}
function documentStyleColorRules(): string[] {
const sheet = (document.getElementById('document-styles') as HTMLStyleElement | null)?.sheet;
if (!sheet) return [];
try {
return Array.from(sheet.cssRules)
.map((r) => r.cssText)
.filter((t) => INLINE_COLOR.test(t) || t.includes('{color:') || t.includes(' color:'))
.slice(0, STYLE_RULE_LIMIT);
} catch {
return [];
}
}
export function getColorDebug(editor: Editor): ColorDebugSnapshot {
const { state } = editor;
const { doc, selection, storedMarks } = state;
@@ -115,6 +193,15 @@ export function getColorDebug(editor: Editor): ColorDebugSnapshot {
? document.documentElement.getAttribute('data-theme')
: null;
// Read on the editor's own element: a page decoration or a theme override lands
// between :root and the text, and only this end of the cascade shows it.
const paintVars: Record<string, string> = {};
if (root && typeof getComputedStyle === 'function') {
const cs = getComputedStyle(root as HTMLElement);
for (const v of PAINT_VARS) paintVars[v] = cs.getPropertyValue(v).trim();
paintVars.color = cs.color;
}
return {
timestamp: new Date().toISOString(),
theme: { dataTheme },
@@ -133,5 +220,10 @@ export function getColorDebug(editor: Editor): ColorDebugSnapshot {
total: domSpansTotal,
sample: domSpansSample,
},
painted: {
vars: paintVars,
colors: root ? paintedColors(root as HTMLElement) : {},
styleRules: typeof document !== 'undefined' ? documentStyleColorRules() : [],
},
};
}