Files
mindfulness/client/src/features/textWrap/overflow/ellipsis.ts
2026-02-10 11:39:33 +08:00

109 lines
3.7 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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 回退单位graphemetoken
*/
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 };
}