109 lines
3.7 KiB
TypeScript
109 lines
3.7 KiB
TypeScript
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<number | null> {
|
||
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 };
|
||
}
|
||
|