更新换行算法和APP-PUSH
This commit is contained in:
92
client/src/features/textWrap/scoring/phraseMatch.ts
Normal file
92
client/src/features/textWrap/scoring/phraseMatch.ts
Normal file
@@ -0,0 +1,92 @@
|
||||
import type { Lang, Token } from '../core/types';
|
||||
import { matchENKeyword, normalizeENKeyword, normalizeWhitespace, tokenizeEN } from '../core/index';
|
||||
import { segmentGraphemes } from '../grapheme/index';
|
||||
|
||||
export type PhraseSpan = { start: number; end: number; length: number };
|
||||
|
||||
function toNonEmptyArray(parts: string[]): string[] {
|
||||
return parts.map((s) => s.trim()).filter((s) => s.length > 0);
|
||||
}
|
||||
|
||||
export function preparePhraseTokens(phrase: string, lang: Lang): string[] {
|
||||
const input = String(phrase ?? '');
|
||||
if (input === '') return [];
|
||||
|
||||
if (lang === 'EN') {
|
||||
const { normalizedText } = normalizeWhitespace(input, 'NORMALIZE');
|
||||
const words = normalizedText.split(' ');
|
||||
// EN:按“全词等值匹配”口径做归一化(小写 + 两端去常见标点)
|
||||
return toNonEmptyArray(words).map((w) => normalizeENKeyword(w));
|
||||
}
|
||||
|
||||
// TC:按 grapheme clusters
|
||||
const { clusters } = segmentGraphemes(input, 'PREFERRED');
|
||||
return clusters;
|
||||
}
|
||||
|
||||
function tokenEqualsPhraseToken(tokenText: string, phraseToken: string, lang: Lang): boolean {
|
||||
if (lang === 'EN') return matchENKeyword(tokenText, phraseToken);
|
||||
return tokenText === phraseToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* 在 tokens 中寻找 phraseTokens 的“连续区间完全匹配”(必须)。
|
||||
*
|
||||
* 输出顺序(确定性):
|
||||
* - start 升序
|
||||
* - start 相同:length 降序(更长优先)
|
||||
*/
|
||||
export function findPhraseSpans(tokens: Token[], phraseTokens: string[], lang: Lang): PhraseSpan[] {
|
||||
const n = tokens.length;
|
||||
const m = phraseTokens.length;
|
||||
if (n === 0 || m === 0 || m > n) return [];
|
||||
|
||||
const spans: PhraseSpan[] = [];
|
||||
|
||||
for (let i = 0; i <= n - m; i++) {
|
||||
let ok = true;
|
||||
for (let j = 0; j < m; j++) {
|
||||
const t = tokens[i + j]?.text ?? '';
|
||||
const p = phraseTokens[j] ?? '';
|
||||
if (!tokenEqualsPhraseToken(t, p, lang)) {
|
||||
ok = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (ok) spans.push({ start: i, end: i + m, length: m });
|
||||
}
|
||||
|
||||
spans.sort((a, b) => {
|
||||
if (a.start !== b.start) return a.start - b.start;
|
||||
return b.length - a.length;
|
||||
});
|
||||
return spans;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断某个 span 是否被断点拆分到不同的行(即:span 内存在 break)。
|
||||
*
|
||||
* 说明:
|
||||
* - span=[start,end) 是 token 索引区间
|
||||
* - breaks[i] 是 token 边界索引(1..N-1)
|
||||
* - 若存在 start < b < end,则 span 被拆分
|
||||
*/
|
||||
export function isSpanSplitByBreaks(span: PhraseSpan, breaks: number[]): boolean {
|
||||
const s = span.start;
|
||||
const e = span.end;
|
||||
if (e - s <= 1) return false;
|
||||
for (const b of breaks) {
|
||||
if (b > s && b < e) return true;
|
||||
if (b >= e) break; // breaks 升序:可提前退出
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅用于测试:把 EN 文本按 core-contract 口径 tokenize。
|
||||
*/
|
||||
export function tokenizePhraseLikeInputEN(text: string): Token[] {
|
||||
const { normalizedText } = normalizeWhitespace(text, 'NORMALIZE');
|
||||
return tokenizeEN(normalizedText);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user