Improve PDF table readability - #14
Conversation
Material draws its table grid with a custom property that only a browser's colour scheme defines. The PDF render never sets one, so the declaration was invalid at computed-value time and the border fell away entirely: a table arrived as unruled columns of text, with the setting name in one column vertically centred and its description top-aligned several lines above, and nothing to say which cell belonged to which row. The grid, the header shading, and the cell padding are drawn here because nothing else will draw them. A dotted setting name has no break opportunity of its own. Left unbreakable it becomes its column's minimum width, and the auto layout pays for it out of the other columns: a three-column table squeezed its description into a ribbon four words wide. `overflow-wrap: anywhere` on the inline code lets the name wrap and gives the column back. That property stays off the cell. On a cell it also collapses the cell's own minimum width, so the column shrinks to one character and the renderer chops whatever is in it: a `Default` heading came back as `Defau` / `lt`, `604800` as `60480` / `0`, and a `Key` column as seven stacked letters. It is also pathologically slow -- a cell holding a page of prose took 9 minutes to lay out where the same document takes 5 seconds, because every line break is re-tried at every character. Cells get `overflow-wrap: break-word`, which breaks a word only once it actually overflows and leaves the minimum width alone. A row that splits leaves its cells misaligned either side of the break, so a row moves whole. The table around it still splits one row at a time, and the two-row guards already on `tbody > tr` still hold. A row too tall for a page of its own splits anyway rather than being dropped. `styles/pdf.css` carries the same fragmentation and wrapping rules, so browser print and PDF export still break a line and a page at the same places. `pdf-test.sh` reads the table fixture back with `pdftotext` twice over. Its line breaks are kept in one reading, because the whitespace-stripped reading every other assertion uses glues a chopped word back together and hides exactly this class of bug; the other reading is `-raw`, which is the only order that reunites a word split inside a cell -- the default order walks the whole row between the two halves. The chopped heading, the unwrapped setting name, a row split across a page, and the content after a row taller than a page are each asserted. The checks fail against the stylesheet this replaces. Closes #13 Co-authored-by: jinjoolee07 <jinjoolee07@gmail.com>
f0d2a02 to
df43a4a
Compare
sehkone
left a comment
There was a problem hiding this comment.
Reviewed, revised, and rebased onto main. Sorry for the force-push over your two commits — the details are below so nothing is lost.
Rebase
main moved after this PR was opened. c3829c4 ("Keep chapter content from dropping out of the PDF", #11) already covers the first commit here, "Improve PDF content flow within chapters", so that commit is dropped as superseded — its section + section, heading, and fragmentation rules are on main in a form that also drops the !importants and adds pdftotext coverage. Only the table work is left, now a single commit on top of current main.
One thing to note about that rebase: the CHANGELOG entries in this PR belonged to the dropped commit, so after the rebase there was no changelog record of the table change at all. New entries are added under both templates.
The issue is real, and worse than #13 describes
Confirmed by rendering the fixture rather than reading the CSS. Material draws the table grid with var(--md-typeset-table-color), a custom property only a browser colour scheme defines. The PDF render never sets one, so the whole border declaration is invalid at computed-value time and falls away: a table arrives as unruled columns of text, with the setting name vertically centred in one column and its description top-aligned several lines above it, and nothing to say which cell belongs to which row. Separately, a long dotted setting name has no break opportunity of its own, so it became its column's minimum width and the auto layout paid for it out of the others — a three-column table squeezed its description into a ribbon four words wide.
So the direction of this PR is right, and the grid, header shading, and cell padding it adds are all kept.
Two defects in the proposed CSS
Both trace to overflow-wrap: anywhere on th/td.
1. It chops words. anywhere also collapses the cell's minimum width, so a column shrinks to one character and the renderer breaks whatever is in it. In the rendered output of this branch: Default came back as Defau / lt, 604800 as 60480 / 0, and a Key column as seven letters stacked vertically. That is the opposite of what the PR is for, and the existing test helpers cannot see it — they strip whitespace before asserting, which glues a chopped word back together.
2. It is pathologically slow. A single cell holding a page of prose took 9 minutes to lay out, against 5 seconds for the same document on main. Isolated rule by rule, overflow-wrap: anywhere on the cell is the sole cause; tr { break-inside: avoid } and hyphens: auto are each ~5s on their own.
The fix is to scope it: cells get overflow-wrap: break-word, which breaks a word only once it actually overflows and leaves the minimum width alone, and anywhere is confined to inline code inside a cell, which is the only thing that needs to break mid-token. Build time is back to 5s and the headings are intact.
Other changes
!importantremoved throughout. The selector already carries two IDs (article:not(#doc-cover):not(#doc-toc)), so it outranks Material's.md-typeset table:not([class])without help — verified by rendering, not by counting. This also matches the reasoningc3829c4recorded for the pagination rules.font-size: 11pt/line-height: 1.25dropped. They contradictedp, li, td, th { font-size: 12pt !important }two hundred lines above in the same file, leaving two conflicting rules for the same elements. Tables read fine at the document size.hyphens: autodropped. #13 does not ask for it, it hyphenates English prose only inside table cells (inconsistent with body text), and it needs a Pyphen dictionary that does not exist forko.- Initial-value redeclarations dropped:
border-spacing,table-layout,word-break,max-width. styles/pdf.cssupdated to match.c3829c4established that the two files must agree on how a block fragments, or browser print and PDF export break at different points. This PR changed fragmentation (tr) and wrapping in the PDF only, so the same rules are now in both.tr { break-inside: avoid }is kept as #13 asks. A row too tall for a page of its own still splits rather than losing its content — verified.
Tests
tests/pdf-test.sh gains a table fixture and four assertions: a long inline-code value wraps and still arrives whole, a heading and a short code value are not broken mid-word, no row is split across a page boundary, and the content after a row taller than a page survives. They fail against the stylesheet this replaces.
Two new readers were needed. One keeps the renderer's line breaks, because the whitespace-stripped reading every other assertion uses hides exactly this class of bug. The other is pdftotext -raw, which is the only order that reunites a word split inside a cell — the default reading order walks the whole row between the two halves.
Known limitation
A single token wider than the page with no break opportunity anywhere in it (no /, -, ., or _) still pushes its table off the page edge, because break-word does not reduce the minimum content width. Checked against realistic content and none of it hits this: a plain-text URL wraps at its slashes, a plain-text dotted setting name wraps, a backticked one wraps, and an eight-column table fits. Only a contrived 106-character unbreakable word overflows. Removing that case means putting anywhere back on the cell, which costs every table its intact headings and the build its 5 seconds, so it is deliberately left.
Verification
Full pdf-test.sh (26 checks), shellcheck, markdownlint, and PDF builds of both samples in en and ko, inspected as rendered pages.
Closes #13