///|
/// Character-filter output plus an offset-correction table. Each entry maps an
/// output UTF-8 byte boundary back to the corresponding input byte offset.
pub struct CharFilterResult {
text : String
corrections : ReadOnlyArray[Int]
}
///|
pub fn CharFilterResult::text(self : CharFilterResult) -> String {
self.text
}
///|
pub(open) trait CharFilter {
fn apply(Self, String) -> CharFilterResult
}
///|
fn analyzer_utf8_width(ch : Char) -> Int {
let value = ch.to_int()
if value <= 0x7F {
1
} else if value <= 0x7FF {
2
} else if value <= 0xFFFF {
3
} else {
4
}
}
///|
fn append_mapped_text(
builder : StringBuilder,
corrections : Array[Int],
replacement : String,
source_start : Int,
source_end : Int,
) -> Unit {
let bytes = @utf8.encode(replacement)
builder.write_string(replacement)
for _ in 0.. CharacterMapping {
{ source, replacement }
}
///|
/// Deterministic scalar-to-string mapping filter with offset correction.
pub struct MappingCharFilter {
mappings : ReadOnlyArray[CharacterMapping]
}
///|
pub fn MappingCharFilter::new(
mappings : Array[CharacterMapping],
) -> MappingCharFilter {
{ mappings: ReadOnlyArray::from_array(mappings.copy()) }
}
///|
pub impl CharFilter for MappingCharFilter with fn apply(self, text) {
let builder = StringBuilder()
let corrections : Array[Int] = [0]
let mut source_offset = 0
for ch in text {
let width = analyzer_utf8_width(ch)
let replacement = match
self.mappings.search_by(mapping => mapping.source == ch) {
Some(index) => self.mappings[index].replacement
None => {
let one = StringBuilder()
one.write_char(ch)
one.to_string()
}
}
append_mapped_text(
builder,
corrections,
replacement,
source_offset,
source_offset + width,
)
source_offset += width
}
{
text: builder.to_string(),
corrections: ReadOnlyArray::from_array(corrections),
}
}
///|
/// Portable compatibility normalization for full-width ASCII and common
/// Unicode spaces. It is intentionally deterministic across all backends.
pub struct CompatibilityNormalizationCharFilter {}
///|
pub fn CompatibilityNormalizationCharFilter::new() -> CompatibilityNormalizationCharFilter {
CompatibilityNormalizationCharFilter::{ }
}
///|
pub impl CharFilter for CompatibilityNormalizationCharFilter with fn apply(
_self,
text,
) {
let builder = StringBuilder()
let corrections : Array[Int] = [0]
let mut source_offset = 0
for ch in text {
let code = ch.to_int()
let normalized = if code >= 0xFF01 && code <= 0xFF5E {
(code - 0xFEE0).unsafe_to_char()
} else if code == 0x3000 || code == 0x00A0 {
' '
} else {
ch
}
let replacement = StringBuilder()
replacement.write_char(normalized)
let width = analyzer_utf8_width(ch)
append_mapped_text(
builder,
corrections,
replacement.to_string(),
source_offset,
source_offset + width,
)
source_offset += width
}
{
text: builder.to_string(),
corrections: ReadOnlyArray::from_array(corrections),
}
}
///|
pub(all) struct SynonymRule {
input : String
output : ReadOnlyArray[String]
} derive(Eq, @debug.Debug)
///|
pub fn SynonymRule::new(input : String, output : Array[String]) -> SynonymRule {
guard input.length() > 0 && output.length() > 0 else {
abort("synonym rules require non-empty input and output")
}
{ input, output: ReadOnlyArray::from_array(output.copy()) }
}
///|
/// Lazy single-input synonym graph filter. Multi-token outputs form an
/// alternate graph path while the original token spans that path.
pub struct SynonymGraphFilter {
rules : ReadOnlyArray[SynonymRule]
expand : Bool
}
///|
pub fn SynonymGraphFilter::new(
rules : Array[SynonymRule],
expand? : Bool = true,
) -> SynonymGraphFilter {
{ rules: ReadOnlyArray::from_array(rules.copy()), expand }
}
///|
priv struct SynonymGraphTokenStream {
input : &TokenStream
rules : ReadOnlyArray[SynonymRule]
expand : Bool
pending : Array[Token]
mut cursor : Int
mut current : Token?
}
///|
fn SynonymGraphTokenStream::load(
self : SynonymGraphTokenStream,
token : Token,
) -> Unit {
self.pending.clear()
self.cursor = 0
let matching : Array[SynonymRule] = []
for rule in self.rules {
if rule.input == token.text {
matching.push(rule)
}
}
let mut maximum_length = 1
for rule in matching {
maximum_length = maximum_length.max(rule.output.length())
}
if self.expand || matching.length() == 0 {
self.pending.push({
text: token.text,
position: token.position,
position_length: maximum_length,
start_offset: token.start_offset,
end_offset: token.end_offset,
})
}
for rule in matching {
for output_index in 0.. {
self.load(token)
if self.pending.length() > 0 {
self.current = Some(self.pending[0])
self.cursor = 1
return true
}
}
None => ()
}
}
self.current = None
false
}
///|
impl TokenStream for SynonymGraphTokenStream with fn token(self) {
self.current
}
///|
pub impl TokenFilter for SynonymGraphFilter with fn transform(self, input) {
SynonymGraphTokenStream::{
input,
rules: self.rules,
expand: self.expand,
pending: [],
cursor: 0,
current: None,
}
}
///|
/// Provenance record for optional user dictionaries and synonym resources.
/// MoonSearch stores only the fingerprint in Schema; applications own the
/// potentially licensed payload and can refuse redistribution explicitly.
pub struct AnalysisResource {
name : String
version : String
license : String
redistributable : Bool
}
///|
pub fn AnalysisResource::new(
name : String,
version : String,
license : String,
redistributable : Bool,
) -> AnalysisResource {
guard name.length() > 0 && version.length() > 0 else {
abort("analysis resource name and version must be non-empty")
}
{ name, version, license, redistributable }
}
///|
pub fn AnalysisResource::fingerprint(self : AnalysisResource) -> String {
self.name + "@" + self.version
}
///|
pub fn AnalysisResource::license(self : AnalysisResource) -> String {
self.license
}
///|
pub fn AnalysisResource::is_redistributable(self : AnalysisResource) -> Bool {
self.redistributable
}