更新换行算法和APP-PUSH
This commit is contained in:
81
client/src/features/textWrap/grapheme/strategies/fallback.ts
Normal file
81
client/src/features/textWrap/grapheme/strategies/fallback.ts
Normal file
@@ -0,0 +1,81 @@
|
||||
import GraphemeSplitter from 'grapheme-splitter';
|
||||
|
||||
/**
|
||||
* fallback 策略:使用 grapheme-splitter 分割 grapheme clusters。
|
||||
*
|
||||
* 选择原因:
|
||||
* - 避免手写不完整的 Unicode 规则导致“漏拆/误拆”
|
||||
* - 作为 Intl.Segmenter 不可用时的稳定兜底
|
||||
*
|
||||
* 注意:
|
||||
* - grapheme-splitter 对少数较新的 emoji ZWJ 序列支持可能不完整(例如 `😮💨`)。
|
||||
* - 为满足本项目“至少不拆 ZWJ sequence/VS16/肤色修饰符/组合字符”的最低要求,这里做一层确定性的后处理合并。
|
||||
*/
|
||||
export function segmentWithFallback(text: string): string[] {
|
||||
const splitter = new GraphemeSplitter();
|
||||
const raw = splitter.splitGraphemes(text);
|
||||
return mergeFallbackClusters(raw);
|
||||
}
|
||||
|
||||
const ZWJ = '\u200D';
|
||||
|
||||
function firstCodePoint(s: string): number | null {
|
||||
if (!s) return null;
|
||||
const cp = s.codePointAt(0);
|
||||
return typeof cp === 'number' ? cp : null;
|
||||
}
|
||||
|
||||
function isVariationSelector(cp: number): boolean {
|
||||
// VS15/VS16
|
||||
return cp === 0xfe0e || cp === 0xfe0f;
|
||||
}
|
||||
|
||||
function isSkinToneModifier(cp: number): boolean {
|
||||
return cp >= 0x1f3fb && cp <= 0x1f3ff;
|
||||
}
|
||||
|
||||
function isCombiningMark(cp: number): boolean {
|
||||
// 常见 combining marks 范围(覆盖 `e\u0301` 等)
|
||||
return (
|
||||
(cp >= 0x0300 && cp <= 0x036f) ||
|
||||
(cp >= 0x1ab0 && cp <= 0x1aff) ||
|
||||
(cp >= 0x1dc0 && cp <= 0x1dff) ||
|
||||
(cp >= 0x20d0 && cp <= 0x20ff) ||
|
||||
(cp >= 0xfe20 && cp <= 0xfe2f)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 对 fallback 输出做“最低可用”合并:
|
||||
* - 若上一个 cluster 以 ZWJ 结尾,则必须与下一个合并(ZWJ sequence)
|
||||
* - 若下一个 cluster 以 VS/肤色修饰符/combining mark 开头,也与上一个合并
|
||||
*
|
||||
* 目标:满足文档列出的“不拆”组合要求,且行为完全确定性。
|
||||
*/
|
||||
function mergeFallbackClusters(raw: string[]): string[] {
|
||||
if (!raw.length) return raw;
|
||||
|
||||
const out: string[] = [];
|
||||
let buf = raw[0] ?? '';
|
||||
|
||||
for (let i = 1; i < raw.length; i++) {
|
||||
const next = raw[i] ?? '';
|
||||
const nextCp = firstCodePoint(next);
|
||||
|
||||
const shouldMerge =
|
||||
buf.endsWith(ZWJ) ||
|
||||
(nextCp !== null && (isVariationSelector(nextCp) || isSkinToneModifier(nextCp) || isCombiningMark(nextCp)));
|
||||
|
||||
if (shouldMerge) {
|
||||
buf += next;
|
||||
continue;
|
||||
}
|
||||
|
||||
out.push(buf);
|
||||
buf = next;
|
||||
}
|
||||
|
||||
out.push(buf);
|
||||
return out;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/**
|
||||
* Intl.Segmenter 策略(优先)
|
||||
*
|
||||
* 注意:不同 JS 引擎/版本对 Intl.Segmenter 的支持可能不同,调用方必须捕获异常并降级。
|
||||
*/
|
||||
|
||||
export function canUseIntlSegmenter(): boolean {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Seg = (globalThis as any)?.Intl?.Segmenter;
|
||||
return typeof Seg === 'function';
|
||||
}
|
||||
|
||||
export function segmentWithIntlSegmenter(text: string): string[] {
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
const Segmenter = (globalThis as any).Intl.Segmenter as new (locales?: string | string[], options?: any) => any;
|
||||
|
||||
// 用 zh-Hant 仅用于选择合适的 locale;grapheme 分割应与语言本身关系不大,但保持固定输入更易对齐跨端。
|
||||
const seg = new Segmenter('zh-Hant', { granularity: 'grapheme' });
|
||||
const it = seg.segment(text);
|
||||
|
||||
const clusters: string[] = [];
|
||||
for (const part of it) {
|
||||
// part: { segment: string, index: number, input: string, isWordLike?: boolean }
|
||||
clusters.push(part.segment);
|
||||
}
|
||||
return clusters;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user