// The suggestion replacement table of a dictionary.
//
// The `.aff` `REP` table is the base. On top of it the manual's `ph:` field
// ("Optional data fields") adds one rule per field: a `ph:` value is a
// transliteration of its dictionary word, so `which ph:wich` is equivalent to
// `REP wich which`. Hunspell calls this the "inner REP table" and gives it the
// same top priority as `REP`.
//
// This is an independent, clean-room implementation of the behaviour documented
// in `hunspell(5)`. No Hunspell (LGPL-2.1) source code was consulted or copied;
// this project is Apache-2.0.
///|
/// The replacement rules the suggestion engine uses: the `.aff` `REP` pairs
/// first, then one rule per `.dic` `ph:` field, in dictionary order.
///
/// The `ph:` forms the manual documents are handled:
///
/// * `ph:wich` — the plain transliteration: rule `wich` -> the word.
/// * `ph:prity*` — a trailing `*` strips the last character of the pattern and
/// of the word: rule `prit` -> `prett`.
/// * `ph:hepi->happi` — an ASCII arrow is an explicit rule `hepi` -> `happi`.
///
/// Every rule is also added in its capitalised and its uppercased spelling (the
/// manual's equivalent `REP` table lists `wendsay Wednesday` *and*
/// `Wendsay Wednesday`; the uppercase variant is what turns an ALL-CAPS
/// misspelling such as `OMG` into `OH, MY GOSH!`).
fn suggest_replacements(
base : Array[@aff.Replacement],
entries : Array[@dic.DicEntry],
ignored : Array[Char],
) -> Array[@aff.Replacement] {
let rules : Array[@aff.Replacement] = []
for replacement in base {
rules.push(replacement)
}
for entry in entries {
let target = remove_ignored(entry.full_word, ignored)
for field in entry.morph {
match field.strip_prefix("ph:") {
Some(value) => add_ph_rules(rules, value.to_owned(), target)
None => ()
}
}
}
rules
}
///|
/// Add the replacement rules one `ph:` value implies for `target`.
fn add_ph_rules(
rules : Array[@aff.Replacement],
value : String,
target : String,
) -> Unit {
let pair : (String, String)? = match value.find("->") {
Some(at) => {
let chars = value.to_array()
let pattern = chars_between(chars, 0, at)
let replacement = chars_between(chars, at + 2, chars.length())
if pattern.is_empty() || replacement.is_empty() {
None
} else {
Some((pattern, replacement))
}
}
None =>
match value.strip_suffix("*") {
Some(body) => {
let pattern = drop_last_char(body.to_owned())
match (pattern, drop_last_char(target)) {
(Some(pattern), Some(stripped)) =>
if pattern.is_empty() {
None
} else {
Some((pattern, stripped))
}
_ => None
}
}
None => if value.is_empty() { None } else { Some((value, target)) }
}
}
match pair {
Some((pattern, replacement)) => push_rule(rules, pattern, replacement)
None => ()
}
}
///|
/// Add the three case spellings of one rule, skipping duplicates.
fn push_rule(
rules : Array[@aff.Replacement],
pattern : String,
replacement : String,
) -> Unit {
if pattern.is_empty() || replacement.is_empty() || pattern == replacement {
return
}
push_unique_rule(rules, pattern, replacement)
let capitalised = capitalise_ascii(pattern)
if capitalised != pattern {
push_unique_rule(rules, capitalised, replacement)
}
let upper = uppercase_ascii(pattern)
if upper != pattern {
push_unique_rule(rules, upper, uppercase_ascii(replacement))
}
}
///|
/// Append `{ from, to, }` unless the exact pair is already present.
fn push_unique_rule(
rules : Array[@aff.Replacement],
from : String,
to : String,
) -> Unit {
for existing in rules {
if existing.from == from && existing.to == to {
return
}
}
rules.push({ from, to, })
}
///|
/// The characters `[start, end)` as a string.
fn chars_between(chars : Array[Char], start : Int, end : Int) -> String {
let buffer = StringBuilder()
let mut index = start
while index < end {
buffer.write_char(chars[index])
index = index + 1
}
buffer.to_string()
}
///|
/// `text` without its last character, or `None` when it is empty.
fn drop_last_char(text : String) -> String? {
let chars = text.to_array()
if chars.length() == 0 {
return None
}
Some(chars_between(chars, 0, chars.length() - 1))
}
///|
/// The ASCII-capitalised form of `text` (first character upper case, the rest
/// left alone).
fn capitalise_ascii(text : String) -> String {
let chars = text.to_array()
if chars.length() == 0 {
return text
}
let buffer = StringBuilder()
buffer.write_char(chars[0].to_ascii_uppercase())
let mut index = 1
while index < chars.length() {
buffer.write_char(chars[index])
index = index + 1
}
buffer.to_string()
}
///|
/// Every ASCII character of `text` upper-cased. Non-ASCII characters pass
/// through unchanged (`moonbitlang/x/unicode` only offers lower-casing).
fn uppercase_ascii(text : String) -> String {
let buffer = StringBuilder()
for c in text {
buffer.write_char(c.to_ascii_uppercase())
}
buffer.to_string()
}