import type { Token } from '../core/types'; import { joinTokens } from '../core/joinTokens'; import { measureWidthCached } from '../measure/measureWidthCached'; import type { ApplyOverflowFallbackInput } from './types'; const DEFAULT_TC_PUNCTS = [',', '。', '!', '?', ';', ':', '、']; export function cleanLineBeforeEllipsis(line: string, lang: 'TC' | 'EN', tcPunctuations?: string[]): string { const s = String(line ?? ''); const trimmed = s.replace(/\s+$/g, ''); if (trimmed === '') return ''; if (lang === 'TC') { const puncts = new Set(tcPunctuations && tcPunctuations.length > 0 ? tcPunctuations : DEFAULT_TC_PUNCTS); const last = trimmed.slice(-1); if (puncts.has(last)) return trimmed.slice(0, -1); } return trimmed; } async function measureIfPossible(args: { text: string; input: ApplyOverflowFallbackInput; }): Promise { const { input, text } = args; if (!input.measure) return null; const enabled = typeof input.measure.measureWidthImpl === 'function' && (input.measure.context === 'APP' || input.measure.widgetEnableMeasure === true); if (!enabled) return null; const res = await measureWidthCached({ text, context: input.measure.context, contextProfile: input.measure.contextProfile, fontSpec: input.measure.fontSpec, measureWidthImpl: input.measure.measureWidthImpl, widgetEnableMeasure: input.measure.widgetEnableMeasure, }); return res.width; } /** * 给最后一行加省略号,并在可测量时保证不超宽: * - EN 回退单位:整词(token) * - TC 回退单位:grapheme(token) */ export async function applyEllipsisToLastLine(args: { input: ApplyOverflowFallbackInput; baseLines: Array<{ start: number; end: number; text: string }>; }): Promise<{ lines: string[] }> { const { input } = args; const maxLines = Math.max(1, input.maxLines | 0); const lines = args.baseLines.slice(0, maxLines); if (lines.length === 0) return { lines: [] }; const lastIdx = lines.length - 1; const last = lines[lastIdx]!; // 先按规则清理,再拼接 ellipsisToken const cleaned = cleanLineBeforeEllipsis(last.text, input.lang, input.tcPunctuations); let candidateText = `${cleaned}${input.ellipsisToken}`; // 若无测量能力:直接输出(确定性) const measured = await measureIfPossible({ text: candidateText, input }); if (measured === null) { const out = lines.map((l, idx) => (idx === lastIdx ? candidateText : l.text)); return { lines: out }; } // 有测量能力:若超宽则按 token 回退 if (measured <= input.availableWidth) { const out = lines.map((l, idx) => (idx === lastIdx ? candidateText : l.text)); return { lines: out }; } // 回退:逐步减少最后一行 token 数再加省略号 let end = last.end; const start = last.start; let foundFit = false; while (end > start) { end -= 1; const base = joinTokens(input.tokens, start, end); const cleaned2 = cleanLineBeforeEllipsis(base, input.lang, input.tcPunctuations); const nextCandidate = `${cleaned2}${input.ellipsisToken}`; const w = await measureIfPossible({ text: nextCandidate, input }); if (w !== null && w <= input.availableWidth) { candidateText = nextCandidate; foundFit = true; break; } } // 若连“仅省略号”都无法满足宽度(极窄场景),也必须给出确定性输出:直接输出 ellipsisToken // 注意:这可能仍然超宽,但已是最小可表达形式;上层可通过 meta 做治理。 if (!foundFit) { candidateText = input.ellipsisToken; } const out = lines.map((l, idx) => (idx === lastIdx ? candidateText : l.text)); return { lines: out }; }