fix: a list number wider than the hanging indent pushes the text

The marker pseudo is floated into the indent instead of absolutely
positioned, so a label the indent cannot hold — a "1.1.1." outline number,
a Roman numeral — moves the first line right the way both word processors
do, rather than printing over the words it labels. Out of flow either way,
so no list line grows.

The PDF clone's injected marker follows suit, and its chain now reads the
effective numbering and walks to the outermost multilevel list: a list
numbered by a named style had none of its own and printed a bare separator.
This commit is contained in:
Steffen Becker committed 2026-09-04 19:30:04 +02:00
1 parent 7a17fc7995
commit 583bc0ddd8
3 files changed
+32 -24

No files matched your search

+12 -12
View File
@@ -142,16 +142,18 @@ function liOrdinal(li: HTMLElement): number {
return parseInt(parent.getAttribute('start') ?? '1', 10) + items.indexOf(li);
}
// The <ol> whose 'multilevel' style governs this list: itself, or the nearest
// ordered ancestor with the attr reached without crossing another explicit style.
// The outermost <ol> of the unbroken 'multilevel' run this list sits in — where the
// chain `counters(list-item, '.')` prints starts, so the walk cannot stop at the nearest
// one. Keyed on the *effective* numbering, the attr editor.css draws from: a list
// numbered by a named style carries no `data-list-style` of its own.
function multilevelRoot(ol: HTMLElement, root: HTMLElement): HTMLElement | null {
let found: HTMLElement | null = null;
for (let e: HTMLElement | null = ol; e && e !== root; e = e.parentElement) {
if (e.tagName !== 'OL') continue;
const t = e.getAttribute('data-list-style');
if (t === 'multilevel') return e;
if (t) return null; // a nearer explicit style cuts the chain
if (e.getAttribute('data-eff-list-style') !== 'multilevel') break;
found = e;
}
return null;
return found;
}
// The marker text the browser renders for a list item: the depth bullet/bulletChar
@@ -203,15 +205,13 @@ function materializeListMarkers(root: HTMLElement): void {
const glyph = listMarkerGlyph(li, root);
if (!glyph) continue;
const target = li.querySelector(':scope > p, :scope > h1, :scope > h2, :scope > h3, :scope > h4, :scope > h5') ?? li;
const slot = document.createElement('span');
slot.style.cssText = 'display:inline-block;width:0;overflow:visible;vertical-align:baseline';
const label = document.createElement('span');
label.style.cssText = `display:inline-block;white-space:pre;transform:translateX(-0.635cm);color:${cs.color}`;
label.style.cssText = `float:left;min-width:0.635cm;margin-left:-0.635cm;white-space:pre;color:${cs.color}`;
// Same symbol shim the editor's ::marker uses (glyphs Liberation Serif lacks).
label.style.fontFamily = `'EdenText Symbols', ${cs.fontFamily}`;
label.textContent = glyph;
slot.appendChild(label);
target.insertBefore(slot, target.firstChild);
label.textContent = `${glyph} `; // the trailing space editor.css puts in every marker
label.dataset.pdfMarker = '';
target.insertBefore(label, target.firstChild);
}
}
+19 -11
View File
@@ -672,17 +672,18 @@
list-style: none;
}
/* Out of flow, so the marker cannot touch the line it labels — in flow it grew every
list line by a pixel, which cost the thesis fixture a page. `top: 0` seats it on the
first line, since the pseudo takes the block's own line-height. */
/* The containing block for the right-aligned marker below, which stays absolute. */
.paper .tiptap li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10) {
position: relative;
}
/* Floated, so the marker never grows the line box (in flow it grew every list line by a
pixel, which cost a fixture a page) yet a label wider than the hanging indent pushes
the first line right instead of printing under it, as both word processors do. */
.paper .tiptap li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
position: absolute;
top: 0;
left: -0.635cm;
float: left;
min-width: 0.635cm;
margin-left: -0.635cm;
white-space: pre;
line-height: inherit;
/* The marker follows the item's first text portion (listMarker.ts sets the custom
@@ -697,7 +698,11 @@
/* A label set against the far end of the hanging indent grows into the margin instead
of into the text — what keeps Word's Roman numerals off the words they label. */
.paper .tiptap :is(ol, ul)[data-marker-align="right"] > li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
left: auto;
float: none;
position: absolute;
top: 0;
min-width: 0;
margin-left: 0;
right: calc(100% + 0.635cm);
}
@@ -752,9 +757,11 @@
font-family: 'EdenText Symbols', var(--marker-family, var(--font-serif)), serif;
}
/* The PDF clone (the .paper itself) replaces every marker with a real span (pdf.ts). */
/* The PDF clone (the .paper itself) replaces every marker with a real span (pdf.ts).
`!important` because the per-numbering rules above are one attribute more specific,
and html2canvas prints what it cannot count — `counters()` as its bare separator. */
.paper.pdf-list-markers li > :is(p, h1, h2, h3, h4, h5, h6, h7, h8, h9, h10)::before {
content: none;
content: none !important;
}
.paper .tiptap li {
@@ -957,8 +964,9 @@
/* After an inline atom ProseMirror's trailing-break BR adds a line the block has not
got, so a paragraph holding an image renders one too tall. Kept where the block really
does end in a break — bare, or wrapped in the mark spans of the run that carried it. */
.paper .tiptap :not(br):not(:has(br:last-child)) + br.ProseMirror-trailingBreak {
does end in a break — bare, wrapped in the mark spans of the run that carried it, or
behind the floated marker the PDF clone injects (which leaves the block empty). */
.paper .tiptap :not(br):not([data-pdf-marker]):not(:has(br:last-child)) + br.ProseMirror-trailingBreak {
display: none;
}