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:
2
assets/dist/seo.js
vendored
2
assets/dist/seo.js
vendored
File diff suppressed because one or more lines are too long
@@ -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. */
|
||||
|
||||
Reference in New Issue
Block a user