Is there a supported fallback when `layout()` undercounts browser lines?
Hi — I'm using Pretext in a paginated document editor, where layout().lineCount feeds directly into the height we reserve for a paragraph or table cell.
I've run into a case where Pretext returns fewer lines than Chrome. Overcounting is usually tolerable for pagination because it just leaves some extra space, but undercounting makes later content look like it still fits and can leave the rendered page overflowing.
I know Pretext isn't trying to reproduce every detail of native layout, and I'm not suggesting that layout() should start reading the DOM. The problem for us is that the result doesn't tell a caller when it has underestimated, so there is no safe fallback unless we already know which text patterns are risky.
Reproduction
I rechecked this on main at 2e5e2bd on 2026-09-17, using installed Chrome on macOS at DPR 2:
text {测试内容.csjg.ysjcxxnr.ypbh}
font 16px "Songti SC", "PingFang SC", serif
lineHeight 20.96
whiteSpace normal
direction ltr
At both widths below, Chrome lays the text out over three lines while Pretext returns two:
| width | Chrome | Pretext main |
|---|---|---|
| 120px | {测试内 / 容.csjg.ysjcxxnr.yp / bh} |
{测试内容.csjg. / ysjcxxnr.ypbh} |
| 135px | {测试内 / 容.csjg.ysjcxxnr.ypbh / } |
{测试内容.csjg. / ysjcxxnr.ypbh} |
Browser side:
<!doctype html>
<html lang="en">
<div id="text" style="width:120px;font:16px/20.96px 'Songti SC','PingFang SC',serif;white-space:normal;word-break:normal;overflow-wrap:break-word">{测试内容.csjg.ysjcxxnr.ypbh}</div>
<script>
console.log(document.querySelector('#text').getBoundingClientRect().height)
// 62.8828125px on my machine: three line boxes
</script>
Pretext side:
import { prepare, layout } from '@chenglou/pretext'
const text = '{测试内容.csjg.ysjcxxnr.ypbh}'
const font = '16px "Songti SC", "PingFang SC", serif'
const prepared = prepare(text, font)
layout(prepared, 120, 20.96) // { lineCount: 2, height: 41.92 }
layout(prepared, 135, 20.96) // { lineCount: 2, height: 41.92 }
The native block height was 62.8828125px at both widths. I also added these two cases temporarily to the repository's wrapping runner with height and lineCount required; both fail against current main in the same way.
The brace case may well be fixable as another line-breaking rule. My broader question is what a correctness-sensitive caller should do when the next native mismatch hasn't been identified yet.
Would a native verification path make sense?
TODO.md currently asks:
Is a slower diagnostic verification mode useful enough to support without changing
layout()?
For pagination, it would be useful if that slower path could return a result we can actually use, rather than only report that the model disagrees with the browser.
For example, it could be an opt-in browser-backed helper, kept completely separate from prepare() and layout(), that verifies one result under an explicit DOM/CSS contract and returns native line ranges when they differ. Another possible shape would be an adapter where the caller supplies the native observation and Pretext handles mapping it back to its range model.
I don't think a general Canvas-only promise that the returned line count is always an upper bound is realistic, especially given the limits described in #321. If native verification doesn't belong in this package either, a documented answer along the lines of "lineCount is a point estimate; callers that cannot tolerate undercounting should do X" would still help. I haven't found an X that doesn't require rebuilding quite a lot of layout plumbing.
What we do today
We currently run a narrow DOM Range probe for text patterns we already know can be risky. It builds an equivalent hidden text block, extracts native line boxes and source offsets, and replaces both the height and break positions when they disagree with Pretext.
That only works when the probe can faithfully reproduce the inline context: no atomic items or extra widths, compatible hard-break handling, and uniform typography. When native probing isn't available, our fallback for plain text is an intentionally loose grapheme-level bound. It avoids undercounting, but it also causes early page breaks and large blank areas.
This came from a real paginated table overflow, not a fuzz-only mismatch. I previously reported #225 and #274; both concrete cases were fixed and let us remove narrower workarounds. This issue is about whether there can be a supported escape hatch for the mismatches we haven't isolated yet.
Happy to provide more production-derived cases or help narrow down an API if this is something you'd consider supporting.
Source: chenglou/pretext