use crate::policy::{Policy, SingletonRule};
use crate::presentation::Presentation;
use crate::scanner::{
EmojiLike, EmojiModification, EmojiSequence, EmojiStem, ScanItem, ScanKind, ZwjJoinedEmoji,
ZwjLink,
};
use crate::unicode;
mod render;
mod types;
use render::{render_flag, render_singleton};
pub use types::{Finding, NonCanonicality};
use types::{ReplacementAnalysis, ReplacementChoice};
#[cfg(test)]
mod tests;
#[must_use]
pub fn analyze_scan_item<'a>(item: &ScanItem<'a>, policy: &Policy) -> Option<Finding<'a>> {
match &item.kind {
ScanKind::Passthrough => None,
ScanKind::UnsanctionedPresentationSelectors(selectors) => Some(Finding::fixed(
item,
NonCanonicality::unsanctioned(selectors.len()),
String::new(),
)),
ScanKind::EmojiSequence(sequence) => match sequence {
EmojiSequence::LinksOnly(links) => analyze_links_only_zwj_sequence(item, links),
EmojiSequence::EmojiHeaded {
first,
joined,
trailing_links,
} => analyze_emoji_headed_sequence(item, first, joined, trailing_links, policy),
},
}
}
fn analyze_emoji_headed_sequence<'a>(
item: &ScanItem<'a>,
first: &EmojiLike,
joined: &[ZwjJoinedEmoji],
trailing_links: &[ZwjLink],
policy: &Policy,
) -> Option<Finding<'a>> {
let mut analysis = ReplacementAnalysis::empty();
analysis += analyze_component(first, policy);
for joined in joined {
analysis += analyze_link(&joined.link);
analysis += analyze_component(&joined.emoji, policy);
}
for link in trailing_links {
analysis += analyze_link(link);
}
if analysis.is_canonical() {
None
} else {
Some(Finding::new(item, analysis))
}
}
fn analyze_links_only_zwj_sequence<'a>(
item: &ScanItem<'a>,
links: &[ZwjLink],
) -> Option<Finding<'a>> {
let mut analysis = ReplacementAnalysis::empty();
for link in links {
analysis += analyze_link(link);
}
if analysis.is_canonical() {
None
} else {
Some(Finding::new(item, analysis))
}
}
fn analyze_link(link: &ZwjLink) -> ReplacementAnalysis {
ReplacementAnalysis::fixed(
NonCanonicality::unsanctioned(link.presentation_selectors_after_link.len()),
unicode::ZWJ.to_string(),
)
}
fn analyze_component(emoji: &EmojiLike, policy: &Policy) -> ReplacementAnalysis {
match &emoji.stem {
EmojiStem::SingletonBase {
base,
presentation_selectors_after_base,
} => analyze_singleton_component(
*base,
presentation_selectors_after_base,
&emoji.modifiers,
policy,
),
EmojiStem::Flag {
first_ri,
presentation_selectors_after_first_ri,
second_ri,
presentation_selectors_after_second_ri,
} => ReplacementAnalysis::fixed(
NonCanonicality::unsanctioned(
presentation_selectors_after_first_ri.len()
+ presentation_selectors_after_second_ri.len()
+ count_selectors_after_modifications(&emoji.modifiers),
),
render_flag(*first_ri, *second_ri, &emoji.modifiers),
),
}
}
fn analyze_singleton_component(
base: char,
presentation_selectors_after_base: &[Presentation],
modifications: &[EmojiModification],
policy: &Policy,
) -> ReplacementAnalysis {
let modification_selector_cleanup =
NonCanonicality::unsanctioned(count_selectors_after_modifications(modifications));
match analyze_singleton_base_selectors(
base,
presentation_selectors_after_base,
modifications.first(),
policy,
) {
SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation,
non_canonicality,
} => ReplacementAnalysis::fixed(
non_canonicality + modification_selector_cleanup,
render_singleton(base, canonical_presentation, modifications),
),
SingletonBaseSelectorOutcome::NeedsPresentationDecision {
default,
non_canonicality,
} => {
let choice = ReplacementChoice::from_replacements(
default,
[
(
Presentation::Text,
render_singleton(base, Some(Presentation::Text), modifications),
),
(
Presentation::Emoji,
render_singleton(base, Some(Presentation::Emoji), modifications),
),
],
);
ReplacementAnalysis::choice(non_canonicality + modification_selector_cleanup, choice)
}
}
}
#[derive(Clone, Copy)]
enum SingletonBaseSelectorOutcome {
Deterministic {
canonical_presentation: Option<Presentation>,
non_canonicality: NonCanonicality,
},
NeedsPresentationDecision {
default: Presentation,
non_canonicality: NonCanonicality,
},
}
fn analyze_singleton_base_selectors(
base: char,
presentation_selectors_after_base: &[Presentation],
first_modification: Option<&EmojiModification>,
policy: &Policy,
) -> SingletonBaseSelectorOutcome {
if !unicode::has_variation_sequence(base) {
return SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: None,
non_canonicality: NonCanonicality::unsanctioned(
presentation_selectors_after_base.len(),
),
};
}
match first_modification {
Some(EmojiModification::EmojiModifier { .. })
if matches!(presentation_selectors_after_base, [Presentation::Text, ..]) =>
{
let [_text, rest @ ..] = presentation_selectors_after_base else {
unreachable!("guard requires leading text presentation")
};
SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: Some(Presentation::Text),
non_canonicality: NonCanonicality::unsanctioned(rest.len()),
}
}
Some(EmojiModification::EmojiModifier { .. }) => {
let non_canonicality = match presentation_selectors_after_base {
[] => NonCanonicality::default(),
[Presentation::Emoji, rest @ ..] => {
let primary = if unicode::is_emoji_modifier_base(base) {
NonCanonicality::MODIFIER_DEFECTIVE_SELECTOR
} else {
NonCanonicality::ADDITIONAL_DEFECTIVE_SELECTOR
};
primary + NonCanonicality::unsanctioned(rest.len())
}
[Presentation::Text, ..] => {
unreachable!("text presentation before modifier is precedence 2")
}
};
SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: None,
non_canonicality,
}
}
Some(EmojiModification::TagModifier(_)) => {
let canonical_form = if unicode::is_emoji_default(base) {
TagBaseCanonicalForm::Bare
} else {
TagBaseCanonicalForm::EmojiSelector
};
let non_canonicality =
analyze_tag_base_selectors(canonical_form, presentation_selectors_after_base);
SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: canonical_form.presentation(),
non_canonicality,
}
}
first_modification => analyze_policy_base_selectors(
base,
presentation_selectors_after_base,
matches!(
first_modification,
Some(EmojiModification::EnclosingKeycap { .. })
),
policy,
),
}
}
#[derive(Clone, Copy)]
enum TagBaseCanonicalForm {
Bare,
EmojiSelector,
}
impl TagBaseCanonicalForm {
fn presentation(self) -> Option<Presentation> {
match self {
Self::Bare => None,
Self::EmojiSelector => Some(Presentation::Emoji),
}
}
}
fn analyze_tag_base_selectors(
canonical_form: TagBaseCanonicalForm,
presentation_selectors_after_base: &[Presentation],
) -> NonCanonicality {
match (canonical_form, presentation_selectors_after_base) {
(TagBaseCanonicalForm::Bare, [])
| (TagBaseCanonicalForm::EmojiSelector, [Presentation::Emoji]) => {
NonCanonicality::default()
}
(TagBaseCanonicalForm::Bare, [Presentation::Emoji, rest @ ..]) => {
NonCanonicality::TAG_REDUNDANT_SELECTOR + NonCanonicality::unsanctioned(rest.len())
}
(TagBaseCanonicalForm::Bare, [Presentation::Text, rest @ ..]) => {
NonCanonicality::TAG_CONFLICTING_SELECTOR + NonCanonicality::unsanctioned(rest.len())
}
(TagBaseCanonicalForm::EmojiSelector, []) => NonCanonicality::TAG_FORCED_PRESENTATION,
(TagBaseCanonicalForm::EmojiSelector, [Presentation::Emoji, rest @ ..]) => {
NonCanonicality::unsanctioned(rest.len())
}
(TagBaseCanonicalForm::EmojiSelector, [Presentation::Text, rest @ ..]) => {
NonCanonicality::TAG_CONFLICTING_SELECTOR
+ NonCanonicality::TAG_FORCED_PRESENTATION
+ NonCanonicality::unsanctioned(rest.len())
}
}
}
fn analyze_policy_base_selectors(
base: char,
presentation_selectors_after_base: &[Presentation],
is_keycap_context: bool,
policy: &Policy,
) -> SingletonBaseSelectorOutcome {
debug_assert!(unicode::has_variation_sequence(base));
match (
policy.singleton_rule(base, is_keycap_context),
presentation_selectors_after_base,
) {
(SingletonRule::TextToBare, &[Presentation::Text, _, ..])
| (SingletonRule::EmojiToBare, &[Presentation::Emoji, _, ..]) => {
SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: None,
non_canonicality: NonCanonicality::unsanctioned(
presentation_selectors_after_base.len() - 1,
) + NonCanonicality::POLICY_REDUNDANT_SELECTOR,
}
}
(_, &[current_presentation, _, ..]) => SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: Some(current_presentation),
non_canonicality: NonCanonicality::unsanctioned(
presentation_selectors_after_base.len() - 1,
),
},
(SingletonRule::BareToEmoji, &[]) => {
SingletonBaseSelectorOutcome::NeedsPresentationDecision {
default: Presentation::Emoji,
non_canonicality: NonCanonicality::PRESENTATION_DECISION,
}
}
(SingletonRule::BareToText, &[]) => {
SingletonBaseSelectorOutcome::NeedsPresentationDecision {
default: Presentation::Text,
non_canonicality: NonCanonicality::PRESENTATION_DECISION,
}
}
(SingletonRule::TextToBare, &[Presentation::Text])
| (SingletonRule::EmojiToBare, &[Presentation::Emoji]) => {
SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: None,
non_canonicality: NonCanonicality::POLICY_REDUNDANT_SELECTOR,
}
}
(
SingletonRule::TextToBare | SingletonRule::BareToText | SingletonRule::BareToEmoji,
&[Presentation::Emoji],
) => SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: Some(Presentation::Emoji),
non_canonicality: NonCanonicality::default(),
},
(
SingletonRule::EmojiToBare | SingletonRule::BareToText | SingletonRule::BareToEmoji,
&[Presentation::Text],
) => SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: Some(Presentation::Text),
non_canonicality: NonCanonicality::default(),
},
(SingletonRule::TextToBare | SingletonRule::EmojiToBare, &[]) => {
SingletonBaseSelectorOutcome::Deterministic {
canonical_presentation: None,
non_canonicality: NonCanonicality::default(),
}
}
}
}
fn count_selectors_after_modification(m: &EmojiModification) -> usize {
match m {
EmojiModification::EmojiModifier {
presentation_selectors_after_modifier,
..
} => presentation_selectors_after_modifier.len(),
EmojiModification::EnclosingKeycap {
presentation_selectors_after_keycap,
} => presentation_selectors_after_keycap.len(),
EmojiModification::TagModifier(runs) => runs
.iter()
.map(|run| run.presentation_selectors_after_tag.len())
.sum(),
}
}
fn count_selectors_after_modifications(modifications: &[EmojiModification]) -> usize {
modifications
.iter()
.map(count_selectors_after_modification)
.sum()
}