evfmt 0.2.0

Emoji Variation Formatter
Documentation
use super::*;
use crate::findings::analyze_scan_item;
use crate::scanner::scan;
use crate::unicode;
use crate::variation_set::VariationSet;
use proptest::prelude::*;

fn default_policy() -> Policy {
    Policy::default()
}

fn bool_variation_set(matches: bool) -> VariationSet {
    if matches {
        VariationSet::all()
    } else {
        VariationSet::none()
    }
}

fn formatted_output(input: &str, policy: &Policy) -> String {
    match format_text(input, policy) {
        FormatResult::Unchanged => input.to_owned(),
        FormatResult::Changed(output) => output,
    }
}

#[test]
fn plain_ascii_is_unchanged() {
    assert_eq!(
        format_text("Hello, world!", &default_policy()),
        FormatResult::Unchanged
    );
}

#[test]
fn standalone_ascii_default_policy_prefers_bare_text_side() {
    let policy = default_policy();

    assert_eq!(format_text("#", &policy), FormatResult::Unchanged);
    assert_eq!(
        format_text("#\u{FE0E}", &policy),
        FormatResult::Changed("#".to_owned())
    );
    assert_eq!(format_text("#\u{FE0F}", &policy), FormatResult::Unchanged);
}

#[test]
fn standalone_text_default_non_ascii_default_policy_resolves_bare_to_emoji() {
    let policy = default_policy();

    assert_eq!(
        format_text("\u{00A9}", &policy),
        FormatResult::Changed("\u{00A9}\u{FE0F}".to_owned())
    );
    assert_eq!(
        format_text("\u{00A9}\u{FE0E}", &policy),
        FormatResult::Unchanged
    );
    assert_eq!(
        format_text("\u{00A9}\u{FE0F}", &policy),
        FormatResult::Unchanged
    );
}

#[test]
fn standalone_emoji_default_non_ascii_default_policy_prefers_bare_emoji_side() {
    let policy = default_policy();

    assert_eq!(format_text("\u{2728}", &policy), FormatResult::Unchanged);
    assert_eq!(
        format_text("\u{2728}\u{FE0E}", &policy),
        FormatResult::Unchanged
    );
    assert_eq!(
        format_text("\u{2728}\u{FE0F}", &policy),
        FormatResult::Changed("\u{2728}".to_owned())
    );
}

#[test]
fn unsanctioned_selectors_are_removed() {
    let policy = default_policy();

    assert_eq!(
        format_text("A\u{FE0F}", &policy),
        FormatResult::Changed("A".to_owned())
    );
    assert_eq!(
        format_text("\u{FE0F}hello", &policy),
        FormatResult::Changed("hello".to_owned())
    );
}

#[test]
fn extra_selector_after_meaningful_selector_is_removed() {
    assert_eq!(
        format_text("#\u{FE0F}\u{FE0E}", &default_policy()),
        FormatResult::Changed("#\u{FE0F}".to_owned())
    );
}

#[test]
fn keycap_cleanup_follows_current_sequence_contract() {
    let policy = default_policy();

    assert_eq!(
        format_text("#\u{FE0F}\u{20E3}", &policy),
        FormatResult::Unchanged
    );
    assert_eq!(
        format_text("#\u{20E3}", &policy),
        FormatResult::Changed("#\u{FE0E}\u{20E3}".to_owned())
    );
    // ZWJ context no longer overrides the component's own keycap policy.
    assert_eq!(
        format_text("#\u{FE0E}\u{20E3}", &policy),
        FormatResult::Unchanged
    );
    assert_eq!(
        format_text("#\u{FE0E}\u{20E3}\u{200D}\u{1F525}", &policy),
        FormatResult::Unchanged
    );
    assert_eq!(
        format_text("\u{26A0}\u{20E3}", &policy),
        FormatResult::Changed("\u{26A0}\u{FE0E}\u{20E3}".to_owned())
    );
    assert_eq!(
        format_text("#\u{20E3}\u{FE0F}", &policy),
        FormatResult::Changed("#\u{FE0E}\u{20E3}".to_owned())
    );
}

#[test]
fn keycap_policy_can_select_bare_text_or_emoji_outputs() {
    let emoji_policy = Policy::default().with_bare_as_text(VariationSet::none());
    assert_eq!(
        format_text("#\u{20E3}", &emoji_policy),
        FormatResult::Changed("#\u{FE0F}\u{20E3}".to_owned())
    );

    let bare_text_policy = Policy::default()
        .with_prefer_bare(crate::variation_set::KEYCAP_CHARS)
        .with_bare_as_text(crate::variation_set::KEYCAP_CHARS);
    assert_eq!(
        format_text("#\u{FE0E}\u{20E3}", &bare_text_policy),
        FormatResult::Changed("#\u{20E3}".to_owned())
    );
    assert_eq!(
        format_text("#\u{20E3}", &bare_text_policy),
        FormatResult::Unchanged
    );
}

#[test]
fn keycap_policy_uses_first_modification_only() {
    let policy = default_policy();

    assert_eq!(
        format_text("#\u{20E3}\u{1F3FB}", &policy),
        FormatResult::Changed("#\u{FE0E}\u{20E3}\u{1F3FB}".to_owned())
    );
    assert_eq!(
        format_text("#\u{20E3}\u{1F3FB}\u{FE0F}", &policy),
        FormatResult::Changed("#\u{FE0E}\u{20E3}\u{1F3FB}".to_owned())
    );
}

#[test]
fn zwj_cleanup_uses_component_local_rules() {
    let policy = default_policy();

    assert_eq!(
        format_text("\u{2764}\u{200D}\u{1F525}", &policy),
        FormatResult::Changed("\u{2764}\u{FE0F}\u{200D}\u{1F525}".to_owned())
    );
    assert_eq!(
        format_text("\u{2764}\u{FE0F}\u{200D}\u{1F525}", &policy),
        FormatResult::Unchanged
    );
    assert_eq!(
        format_text("\u{2764}\u{FE0E}\u{200D}\u{1F525}", &policy),
        FormatResult::Unchanged
    );
    assert_eq!(
        format_text("\u{1F600}\u{FE0F}\u{200D}\u{1F525}", &policy),
        FormatResult::Changed("\u{1F600}\u{200D}\u{1F525}".to_owned())
    );
}

#[test]
fn mixed_content_formats_only_structural_items() {
    let policy = default_policy();

    assert_eq!(
        format_text("Press # for \u{00A9}", &policy),
        FormatResult::Changed("Press # for \u{00A9}\u{FE0F}".to_owned())
    );
}

#[derive(Debug, Clone, Copy)]
enum InputSelector {
    None,
    Text,
    Emoji,
}

#[derive(Debug, Clone, Copy)]
struct PolicyFlags {
    prefer_bare: bool,
    bare_as_text: bool,
}

fn build_input(ch: char, selector: InputSelector) -> String {
    let mut input = String::new();
    input.push(ch);
    match selector {
        InputSelector::None => {}
        InputSelector::Text => input.push(unicode::TEXT_PRESENTATION_SELECTOR),
        InputSelector::Emoji => input.push(unicode::EMOJI_PRESENTATION_SELECTOR),
    }
    input
}

fn expected_standalone(ch: char, selector: InputSelector, policy: PolicyFlags) -> String {
    let mut output = String::new();
    output.push(ch);

    match (policy.prefer_bare, policy.bare_as_text, selector) {
        (true, true, InputSelector::Text)
        | (true, false, InputSelector::Emoji)
        | (true, _, InputSelector::None) => {}
        (false, true, InputSelector::None) | (_, _, InputSelector::Text) => {
            output.push(unicode::TEXT_PRESENTATION_SELECTOR);
        }
        (false, false, InputSelector::None) | (_, _, InputSelector::Emoji) => {
            output.push(unicode::EMOJI_PRESENTATION_SELECTOR);
        }
    }

    output
}

#[test]
fn exhaustive_standalone_variation_sequence_policy_table() {
    let policies = [
        PolicyFlags {
            prefer_bare: false,
            bare_as_text: false,
        },
        PolicyFlags {
            prefer_bare: false,
            bare_as_text: true,
        },
        PolicyFlags {
            prefer_bare: true,
            bare_as_text: false,
        },
        PolicyFlags {
            prefer_bare: true,
            bare_as_text: true,
        },
    ];
    let selectors = [
        InputSelector::None,
        InputSelector::Text,
        InputSelector::Emoji,
    ];

    for ch in unicode::variation_sequence_chars() {
        for policy_flags in policies {
            let policy = Policy::default()
                .with_prefer_bare(bool_variation_set(policy_flags.prefer_bare))
                .with_bare_as_text(bool_variation_set(policy_flags.bare_as_text));

            for selector in selectors {
                let input = build_input(ch, selector);
                let actual = formatted_output(&input, &policy);
                let expected = expected_standalone(ch, selector, policy_flags);

                assert_eq!(
                    actual, expected,
                    "U+{:04X} selector={selector:?} policy={policy_flags:?}",
                    ch as u32
                );
            }
        }
    }
}

fn interesting_string_strategy() -> impl Strategy<Value = String> {
    let token = prop_oneof![
        3 => prop::sample::select(vec![
            '\u{0023}', '\u{002A}', '\u{0030}', '\u{00A9}', '\u{00AE}',
            '\u{203C}', '\u{2049}', '\u{2122}', '\u{2139}', '\u{2764}',
        ]),
        3 => prop::sample::select(vec![
            '\u{231A}', '\u{2728}', '\u{2614}', '\u{26A1}', '\u{2705}',
            '\u{270A}', '\u{2B50}', '\u{1F004}', '\u{1F600}',
            '\u{1F468}', '\u{1F466}', '\u{1F525}',
        ]),
        4 => prop::sample::select(vec![
            unicode::TEXT_PRESENTATION_SELECTOR,
            unicode::EMOJI_PRESENTATION_SELECTOR,
        ]),
        3 => prop::sample::select(vec![unicode::ZWJ, unicode::COMBINING_ENCLOSING_KEYCAP]),
        1 => prop::sample::select(vec![
            '\u{1F3FB}', '\u{1F3FC}', '\u{1F3FD}', '\u{1F3FE}', '\u{1F3FF}',
        ]),
        2 => prop::sample::select(vec![
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J',
            'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T',
            'U', 'V', 'W', 'X', 'Y', 'Z',
        ]),
        1 => prop::sample::select(vec!['\n', ' ']),
    ];

    prop::collection::vec(token, 0..40).prop_map(|chars| chars.into_iter().collect())
}

fn policy_strategy() -> impl Strategy<Value = Policy> {
    (prop::bool::ANY, prop::bool::ANY).prop_map(|(prefer_bare, bare_as_text)| {
        Policy::default()
            .with_prefer_bare(bool_variation_set(prefer_bare))
            .with_bare_as_text(bool_variation_set(bare_as_text))
    })
}

fn strip_selectors(s: &str) -> String {
    s.chars()
        .filter(|&ch| {
            ch != unicode::TEXT_PRESENTATION_SELECTOR && ch != unicode::EMOJI_PRESENTATION_SELECTOR
        })
        .collect()
}

proptest! {
    #[test]
    fn prop_idempotent(input in interesting_string_strategy(), policy in policy_strategy()) {
        let first = formatted_output(&input, &policy);
        prop_assert_eq!(format_text(&first, &policy), FormatResult::Unchanged);
    }

    #[test]
    fn prop_no_findings_in_output(
        input in interesting_string_strategy(),
        policy in policy_strategy(),
    ) {
        let output = formatted_output(&input, &policy);
        for item in scan(&output) {
            if let Some(finding) = analyze_scan_item(&item, &policy) {
                prop_assert!(
                    false,
                    "finding remains after formatting: {finding:?} for item {item:?}"
                );
            }
        }
    }

    #[test]
    fn prop_only_modifies_selectors(input in interesting_string_strategy(), policy in policy_strategy()) {
        let output = formatted_output(&input, &policy);
        prop_assert_eq!(strip_selectors(&input), strip_selectors(&output));
    }
}