From 6328b252093590f2c220054338391ee53663d1ac Mon Sep 17 00:00:00 2001 From: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:36:17 -0500 Subject: [PATCH 1/3] proof: emoji flags followed by U+FE0F load (RLA-ES Spanish) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit RLA-ES, the Spanish Hunspell dictionaries in LibreOffice and most Linux distributions, writes one flag as U+260E followed by U+FE0F, in the rules (PFX ☎️ Y 3) and on stems (banco/S☎️). parse_aff rejected it as "not one character", so the whole dictionary failed to load. The variation selector is now dropped wherever flags are read, with a regression test. Co-Authored-By: Claude Opus 5.5 --- crates/proof/src/affix.rs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/crates/proof/src/affix.rs b/crates/proof/src/affix.rs index 1cbf3d1df..48a5e506e 100644 --- a/crates/proof/src/affix.rs +++ b/crates/proof/src/affix.rs @@ -266,6 +266,11 @@ fn zero_empty(s: &str) -> String { if s == "0" { String::new() } else { s.to_string() } } +/// U+FE0F VARIATION SELECTOR-16 asks for the emoji form of the character before it. A dictionary +/// that uses an emoji as a flag may write it after the flag (RLA-ES Spanish: `PFX ☎️`, `banco/S☎️`), +/// but it is not a flag of its own: it is dropped wherever flags are read. +const EMOJI_SELECTOR: char = '\u{fe0f}'; + /// Read Hunspell `.aff` text (already decoded to UTF-8). fn parse_aff(aff: &str) -> Result { let mut r = Rules::default(); @@ -320,7 +325,7 @@ fn parse_aff(aff: &str) -> Result { } "PFX" | "SFX" => { let suffix = dir == "SFX"; - let mut flag_chars = at(1).chars(); + let mut flag_chars = at(1).chars().filter(|&c| c != EMOJI_SELECTOR); let (Some(flag), None) = (flag_chars.next(), flag_chars.next()) else { return Err(format!("line {}: flag `{}` is not one character", n + 1, at(1))); }; @@ -377,7 +382,9 @@ impl AffixDict { let mut in_flags = false; while let Some(c) = chars.next() { if in_flags { - flags.push(c); + if c != EMOJI_SELECTOR { + flags.push(c); + } } else if c == '\\' && chars.peek() == Some(&'/') { word.push('/'); chars.next(); @@ -1079,6 +1086,18 @@ pub(crate) mod tests { assert!(AffixDict::from_hunspell("FLAG UTF-8\n", "0\n", &[]).is_ok(), "FLAG alone is harmless"); } + #[test] + fn emoji_flags_with_a_variation_selector_load() { + // RLA-ES (the Spanish dictionaries in LibreOffice and most Linux distributions) writes its + // telephone flag as ☎ followed by U+FE0F, in the rule and on the stems. + let aff = "SET UTF-8\nFLAG UTF-8\nPFX \u{260e}\u{fe0f} Y 1\nPFX \u{260e}\u{fe0f} 0 tele .\nSFX S Y 1\nSFX S 0 s [aeiou]\n"; + let d = AffixDict::from_hunspell(aff, "2\nbanco/S\u{260e}\u{fe0f}\nbasura/S\n", &[]).unwrap(); + assert!(d.contains("banco") && d.contains("bancos")); + assert!(d.contains("telebanco") && d.contains("telebancos"), "the ☎ prefix applies"); + assert!(d.contains("basuras") && !d.contains("telebasura"), "only to stems that carry it"); + assert!(!d.flag_chars.contains(&EMOJI_SELECTOR), "the selector is not a flag"); + } + #[test] fn more_than_64_distinct_flags_load() { // VERO pt_BR uses 106 distinct flags (FLAG UTF-8). Build one with every basic Latin From fd91a78e71c0079954e7cb56b0bf73dc960faacf Mon Sep 17 00:00:00 2001 From: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Date: Thu, 15 Oct 2026 12:39:04 -0500 Subject: [PATCH 2/3] contributors: add dmim2502-commits (display name Mulai) --- contributors/people.toml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/contributors/people.toml b/contributors/people.toml index c4639af25..0f9aac3b4 100644 --- a/contributors/people.toml +++ b/contributors/people.toml @@ -166,3 +166,6 @@ real_name = "Max Murphy" [people.SlavenDj] sync_github_name = true + +[people.dmim2502-commits] +display_name = "Mulai" From 40ae6ac08c68f6f9d8d2ed2569cadc470f034ef2 Mon Sep 17 00:00:00 2001 From: dmim2502-commits <234869083+dmim2502-commits@users.noreply.github.com> Date: Wed, 21 Oct 2026 16:21:24 -0500 Subject: [PATCH 3/3] proof: drop U+FE0F from FORBIDDENWORD and NOSUGGEST flags too Review follow-up: the directives that name one flag read the same emoji form, so they now skip the variation selector like PFX/SFX and stem flags. Co-Authored-By: Claude Opus 5.5 --- crates/proof/src/affix.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/crates/proof/src/affix.rs b/crates/proof/src/affix.rs index 48a5e506e..2c281c7ca 100644 --- a/crates/proof/src/affix.rs +++ b/crates/proof/src/affix.rs @@ -295,7 +295,7 @@ fn parse_aff(aff: &str) -> Result { } } directive @ ("FORBIDDENWORD" | "NOSUGGEST") => { - let mut it = at(1).chars(); + let mut it = at(1).chars().filter(|&c| c != EMOJI_SELECTOR); match (it.next(), it.next()) { (Some(c), None) if !c.is_whitespace() => { if directive == "FORBIDDENWORD" { @@ -1098,6 +1098,15 @@ pub(crate) mod tests { assert!(!d.flag_chars.contains(&EMOJI_SELECTOR), "the selector is not a flag"); } + #[test] + fn emoji_forbiddenword_and_nosuggest_flags_load() { + // The same selector after the flag of the directives that name one flag. + let aff = "SET UTF-8\nFLAG UTF-8\nFORBIDDENWORD \u{260e}\u{fe0f}\nNOSUGGEST \u{1f6ab}\u{fe0f}\n"; + let d = AffixDict::from_hunspell(aff, "3\nbanco\nbanko/\u{260e}\u{fe0f}\nbancu/\u{1f6ab}\u{fe0f}\n", &[]).unwrap(); + assert!(d.contains("banco") && d.contains("bancu")); + assert!(!d.contains("banko"), "a stem with the FORBIDDENWORD flag is not a word"); + } + #[test] fn more_than_64_distinct_flags_load() { // VERO pt_BR uses 106 distinct flags (FLAG UTF-8). Build one with every basic Latin