///|
/// Selects one deterministic search-key representation.
pub(all) enum IndexProfile {
ExactNormalized
FullPinyin
Initials
Compact
} derive(Eq, Debug)
///|
/// One search-key fragment tied to its original source span.
pub struct IndexSegment {
value_text : String
source_span : SourceSpan
source_kind : TokenKind
} derive(Eq, Debug)
///|
/// A normalized key plus auditable source mapping.
pub struct IndexKey {
key_text : String
profile_value : IndexProfile
segment_values : Array[IndexSegment]
} derive(Eq, Debug)
///|
fn ascii_lower(code : Int) -> Int {
if code >= 65 && code <= 90 {
code + 32
} else {
code
}
}
///|
fn is_folded_punctuation(code : Int) -> Bool {
(code >= 0 && code <= 47) ||
(code >= 58 && code <= 64) ||
(code >= 91 && code <= 96) ||
(code >= 123 && code <= 127) ||
code == 0x3000 ||
code == 0x3001 ||
code == 0x3002 ||
(code >= 0xFF01 && code <= 0xFF0F) ||
(code >= 0xFF1A && code <= 0xFF20) ||
(code >= 0xFF3B && code <= 0xFF40) ||
(code >= 0xFF5B && code <= 0xFF65)
}
///|
fn normalize_literal(value : String) -> String {
let output_codes : Array[Int] = []
for code in text_code_points(value) {
if !is_folded_punctuation(code) {
output_codes.push(ascii_lower(code))
}
}
code_points_text(output_codes)
}
///|
fn token_plain_pinyin(token : ConvertedToken) -> String {
let candidates = token.candidates()
if candidates.length() > 0 {
match parse_syllable(candidates[0]) {
Ok(syllable) => syllable.format(PlainPinyin, Lowercase)
Err(_) => normalize_literal(token.rendered())
}
} else {
normalize_literal(token.rendered())
}
}
///|
fn token_initial(token : ConvertedToken) -> String {
let candidates = token.candidates()
if candidates.length() > 0 {
match parse_syllable(candidates[0]) {
Ok(syllable) => {
let plain = syllable.format(PlainPinyin, Lowercase)
if plain.length() == 0 {
""
} else {
plain[0].unsafe_to_char().to_string()
}
}
Err(_) => normalize_literal(token.rendered())
}
} else {
normalize_literal(token.rendered())
}
}
///|
fn index_segment_value(
token : ConvertedToken,
profile : IndexProfile,
) -> String {
match profile {
ExactNormalized => normalize_literal(token.original())
FullPinyin | Compact =>
if token.kind() == PinyinToken {
token_plain_pinyin(token)
} else {
normalize_literal(token.original())
}
Initials =>
if token.kind() == PinyinToken {
token_initial(token)
} else {
normalize_literal(token.original())
}
}
}
///|
fn needs_key_separator(
profile : IndexProfile,
previous : TokenKind?,
current : TokenKind,
) -> Bool {
match profile {
Compact | ExactNormalized => false
FullPinyin =>
match previous {
Some(previous_kind) =>
previous_kind == PinyinToken || current == PinyinToken
None => false
}
Initials =>
match previous {
Some(previous_kind) => previous_kind != current
None => false
}
}
}
///|
/// Generates a deterministic search key from an existing conversion result.
pub fn generate_index_key(
converted : ConversionResult,
profile : IndexProfile,
) -> IndexKey {
let segments : Array[IndexSegment] = []
let mut key = ""
let mut previous_kind : TokenKind? = None
for token in converted.tokens() {
let value = index_segment_value(token, profile)
if value.length() > 0 {
if needs_key_separator(profile, previous_kind, token.kind()) {
key = key + " "
}
key = key + value
segments.push({
value_text: value,
source_span: token.span(),
source_kind: token.kind(),
})
previous_kind = Some(token.kind())
}
}
{ key_text: key, profile_value: profile, segment_values: segments }
}
///|
pub fn IndexSegment::value(self : IndexSegment) -> String {
self.value_text
}
///|
pub fn IndexSegment::span(self : IndexSegment) -> SourceSpan {
self.source_span
}
///|
pub fn IndexSegment::kind(self : IndexSegment) -> TokenKind {
self.source_kind
}
///|
pub fn IndexKey::key(self : IndexKey) -> String {
self.key_text
}
///|
pub fn IndexKey::profile(self : IndexKey) -> IndexProfile {
self.profile_value
}
///|
pub fn IndexKey::segments(self : IndexKey) -> Array[IndexSegment] {
self.segment_values.copy()
}