63 lines
2.1 KiB
TypeScript
63 lines
2.1 KiB
TypeScript
import { pinyin } from "pinyin-pro";
|
|
|
|
const HAN_CHAR = /\p{Script=Han}/u;
|
|
const ASCII_ALNUM = /[a-z0-9]/i;
|
|
const ASCII_QUERY = /^[a-z0-9]+$/;
|
|
|
|
/**
|
|
* Highlight ranges (flat [from, to, ...] UTF-16 offsets into `label`) for a
|
|
* completion query, used when CodeMirror's built-in filter is bypassed.
|
|
*
|
|
* Tiers: case-insensitive substring → pinyin initials ("zzj" highlights each
|
|
* Han character of 总租金) → in-order fuzzy. Returns [] when nothing matches.
|
|
*/
|
|
export function completionMatchRanges(label: string, query: string): number[] {
|
|
if (!query) return [];
|
|
const lowerLabel = label.toLowerCase();
|
|
const lowerQuery = query.toLowerCase();
|
|
|
|
const direct = lowerLabel.indexOf(lowerQuery);
|
|
if (direct >= 0) return [direct, direct + lowerQuery.length];
|
|
|
|
if (ASCII_QUERY.test(lowerQuery) && HAN_CHAR.test(label)) {
|
|
const pinyinRanges = pinyinInitialMatchRanges(label, lowerQuery);
|
|
if (pinyinRanges) return pinyinRanges;
|
|
}
|
|
|
|
return fuzzyMatchRanges(lowerLabel, lowerQuery);
|
|
}
|
|
|
|
function pinyinInitialMatchRanges(label: string, query: string): number[] | null {
|
|
// Greedy earliest subsequence over the pinyin-initial positions, so both
|
|
// prefix queries ("zz") and skipping queries ("zj") highlight the matched
|
|
// Han characters.
|
|
const ranges: number[] = [];
|
|
let queryIndex = 0;
|
|
let index = 0;
|
|
for (const char of label) {
|
|
if (queryIndex >= query.length) break;
|
|
const isHan = HAN_CHAR.test(char);
|
|
if (isHan || ASCII_ALNUM.test(char)) {
|
|
const initial = isHan ? pinyin(char, { pattern: "first", toneType: "none" }) : char.toLowerCase();
|
|
if (initial === query[queryIndex]) {
|
|
ranges.push(index, index + char.length);
|
|
queryIndex += 1;
|
|
}
|
|
}
|
|
index += char.length;
|
|
}
|
|
return queryIndex === query.length ? ranges : null;
|
|
}
|
|
|
|
function fuzzyMatchRanges(lowerLabel: string, lowerQuery: string): number[] {
|
|
const ranges: number[] = [];
|
|
let from = 0;
|
|
for (const char of lowerQuery) {
|
|
const position = lowerLabel.indexOf(char, from);
|
|
if (position < 0) return [];
|
|
ranges.push(position, position + char.length);
|
|
from = position + char.length;
|
|
}
|
|
return ranges;
|
|
}
|