fix: the result preview truncates at the last word boundary

UI-SPEC E-1: a title or description wider than its pixel budget is cut
at the last word boundary that fits, then " …" is appended. The preview
cut inside a word and appended "…" without a space. A first word too
wide on its own is still cut inside the word. Bundle rebuilt.
This commit is contained in:
2026-10-27 13:23:20 +01:00
parent 9e45ddc848
commit 9461c74a80
2 changed files with 38 additions and 8 deletions

2
assets/dist/seo.js vendored

File diff suppressed because one or more lines are too long

View File

@@ -62,23 +62,53 @@ export function textWidth(text: string, font: string): number {
return estimate(text, size)
}
/** text cut to fit maxPx with an ellipsis, or text itself when it fits. */
/** The mark appended to a truncated preview line (UI-SPEC E-1). */
export const ELLIPSIS = ' …'
/**
* text cut at the last word boundary that fits maxPx, then ELLIPSIS, or
* text itself when it fits (UI-SPEC E-1). A first word too wide on its own
* is cut inside the word.
*/
export function truncate(text: string, font: string, maxPx: number): string {
if (textWidth(text, font) <= maxPx) {
return text
}
const fits = (s: string): boolean => textWidth(s + ELLIPSIS, font) <= maxPx
const chars = Array.from(text)
// The word ends: where a run of spaces starts after a word.
const ends: number[] = []
for (let i = 1; i < chars.length; i++) {
if (/\s/u.test(chars[i]) && !/\s/u.test(chars[i - 1])) {
ends.push(i)
}
}
let best = -1
let lo = 0
let hi = chars.length
while (lo < hi) {
const mid = Math.ceil((lo + hi) / 2)
if (textWidth(chars.slice(0, mid).join('').trimEnd() + '…', font) <= maxPx) {
lo = mid
let hi = ends.length - 1
while (lo <= hi) {
const mid = (lo + hi) >> 1
if (fits(chars.slice(0, ends[mid]).join(''))) {
best = mid
lo = mid + 1
} else {
hi = mid - 1
}
}
return chars.slice(0, lo).join('').trimEnd() + '…'
if (best >= 0) {
return chars.slice(0, ends[best]).join('') + ELLIPSIS
}
let cut = 0
let top = chars.length
while (cut < top) {
const mid = Math.ceil((cut + top) / 2)
if (fits(chars.slice(0, mid).join('').trimEnd())) {
cut = mid
} else {
top = mid - 1
}
}
return chars.slice(0, cut).join('').trimEnd() + ELLIPSIS
}
/** The font and pixel limit of a measured field. */