alix 0.8.0

A learning tool built for understanding, not just remembering.
Documentation
use unicode_segmentation::UnicodeSegmentation;

use crate::invisible::{Survey, survey};

#[derive(Clone, Copy)]
enum Target {
    Zwj,
    VariationSelector,
    Tag,
    Mvs,
}

impl Target {
    fn contains(self, ch: char) -> bool {
        match self {
            Self::Zwj => ch == '\u{200d}',
            Self::VariationSelector => matches!(ch, '\u{fe0e}' | '\u{fe0f}'),
            Self::Tag => ('\u{e0020}'..='\u{e007f}').contains(&ch),
            Self::Mvs => ch == '\u{180e}',
        }
    }

    fn reported(self, found: &Survey) -> bool {
        match self {
            Self::Zwj => found.counts.contains_key("ZWJ"),
            Self::VariationSelector => {
                found.counts.contains_key("VS15") || found.counts.contains_key("VS16")
            }
            Self::Tag => found.stray_tags > 0,
            Self::Mvs => found.counts.contains_key("MVS"),
        }
    }
}

struct Row {
    label: &'static str,
    text: &'static str,
    target: Target,
    anomaly: bool,
}

#[test]
fn the_emoji_zoo_flags_only_invisibles_outside_well_formed_sequences() {
    let rows = [
        Row {
            label: "ZWJ woman technologist",
            text: "\u{1f469}\u{200d}\u{1f4bb}",
            target: Target::Zwj,
            anomaly: false,
        },
        Row {
            label: "ZWJ family chain",
            text: "\u{1f468}\u{200d}\u{1f469}\u{200d}\u{1f467}\u{200d}\u{1f466}",
            target: Target::Zwj,
            anomaly: false,
        },
        Row {
            label: "ZWJ heart on fire after VS16",
            text: "\u{2764}\u{fe0f}\u{200d}\u{1f525}",
            target: Target::Zwj,
            anomaly: false,
        },
        Row {
            label: "ZWJ skin-tone technologist",
            text: "\u{1f469}\u{1f3fd}\u{200d}\u{1f4bb}",
            target: Target::Zwj,
            anomaly: false,
        },
        Row {
            label: "ZWJ orphan",
            text: "\u{200d}",
            target: Target::Zwj,
            anomaly: true,
        },
        Row {
            label: "ZWJ after a non-emoji base",
            text: "\u{41}\u{200d}\u{1f4bb}",
            target: Target::Zwj,
            anomaly: true,
        },
        Row {
            label: "ZWJ before a non-emoji base",
            text: "\u{1f469}\u{200d}\u{41}",
            target: Target::Zwj,
            anomaly: true,
        },
        Row {
            label: "ZWJ between Extended_Pictographic symbols outside Emoji=Yes",
            text: "\u{2388}\u{200d}\u{2388}",
            target: Target::Zwj,
            anomaly: false,
        },
        Row {
            label: "ZWJ after an orphan skin-tone component",
            text: "\u{1f3fd}\u{200d}\u{1f4bb}",
            target: Target::Zwj,
            anomaly: true,
        },
        Row {
            label: "ZWJ after a keycap digit without its enclosing keycap",
            text: "\u{31}\u{200d}\u{1f4bb}",
            target: Target::Zwj,
            anomaly: true,
        },
        Row {
            label: "ZWJ between regional-indicator components",
            text: "\u{1f1e6}\u{200d}\u{1f1e7}",
            target: Target::Zwj,
            anomaly: true,
        },
        Row {
            label: "ZWJ followed by a variation selector before the pictograph",
            text: "\u{1f469}\u{200d}\u{fe0f}\u{1f4bb}",
            target: Target::Zwj,
            anomaly: true,
        },
        Row {
            label: "VS15 heart text presentation",
            text: "\u{2764}\u{fe0e}",
            target: Target::VariationSelector,
            anomaly: false,
        },
        Row {
            label: "VS16 heart emoji presentation",
            text: "\u{2764}\u{fe0f}",
            target: Target::VariationSelector,
            anomaly: false,
        },
        Row {
            label: "VS16 coffee emoji presentation",
            text: "\u{2615}\u{fe0f}",
            target: Target::VariationSelector,
            anomaly: false,
        },
        Row {
            label: "VS15 after a non-emoji base",
            text: "\u{41}\u{fe0e}",
            target: Target::VariationSelector,
            anomaly: true,
        },
        Row {
            label: "VS16 after a non-emoji base",
            text: "\u{41}\u{fe0f}",
            target: Target::VariationSelector,
            anomaly: true,
        },
        Row {
            label: "VS16 orphan",
            text: "\u{fe0f}",
            target: Target::VariationSelector,
            anomaly: true,
        },
        Row {
            label: "TAG England flag",
            text: "\u{1f3f4}\u{e0067}\u{e0062}\u{e0065}\u{e006e}\u{e0067}\u{e007f}",
            target: Target::Tag,
            anomaly: false,
        },
        Row {
            label: "TAG Scotland flag",
            text: "\u{1f3f4}\u{e0067}\u{e0062}\u{e0073}\u{e0063}\u{e0074}\u{e007f}",
            target: Target::Tag,
            anomaly: false,
        },
        Row {
            label: "TAG Wales flag",
            text: "\u{1f3f4}\u{e0067}\u{e0062}\u{e0077}\u{e006c}\u{e0073}\u{e007f}",
            target: Target::Tag,
            anomaly: false,
        },
        Row {
            label: "TAG letters outside a flag",
            text: "\u{41}\u{e0061}\u{e0062}\u{e007f}",
            target: Target::Tag,
            anomaly: true,
        },
        Row {
            label: "TAG flag without cancel",
            text: "\u{1f3f4}\u{e0067}\u{e0062}\u{e0065}\u{e006e}\u{e0067}",
            target: Target::Tag,
            anomaly: true,
        },
        Row {
            label: "TAG cancel outside a flag",
            text: "\u{e007f}",
            target: Target::Tag,
            anomaly: true,
        },
        Row {
            label: "TAG sequence after the wrong flag base",
            text: "\u{1f3f3}\u{e0067}\u{e0062}\u{e0065}\u{e006e}\u{e0067}\u{e007f}",
            target: Target::Tag,
            anomaly: true,
        },
        Row {
            label: "MVS inside Mongolian text",
            text: "\u{182c}\u{180e}\u{1820}",
            target: Target::Mvs,
            anomaly: false,
        },
        Row {
            label: "MVS alone still grades and never reports",
            text: "\u{180e}",
            target: Target::Mvs,
            anomaly: false,
        },
    ];

    let mut wrong = Vec::new();
    for row in rows {
        let targets = row
            .text
            .chars()
            .filter(|ch| row.target.contains(*ch))
            .count();
        assert!(targets > 0, "{}: row has no target scalar", row.label);

        if row.label.contains("Extended_Pictographic") {
            assert_eq!(
                row.text.graphemes(true).count(),
                1,
                "{}: GB11 must recognize the pictographic ZWJ grapheme",
                row.label
            );
        }

        let found = survey(row.text);
        let got = row.target.reported(&found);
        if got != row.anomaly {
            wrong.push(format!(
                "{}: expected reported {}, got {} from {:?}",
                row.label, row.anomaly, got, found
            ));
        }
    }

    assert!(wrong.is_empty(), "{wrong:#?}");
}

#[test]
fn the_emoji_zoo_source_is_printable_ascii_plus_lf() {
    // A CRLF pair is the checkout's line-ending translation, not an authored
    // byte, so it is undone before the audit; a lone CR still fails.
    let wrong = include_str!("emoji_zoo.rs")
        .replace("\r\n", "\n")
        .bytes()
        .enumerate()
        .filter(|(_, byte)| *byte != b'\n' && !(b' '..=b'~').contains(byte))
        .collect::<Vec<_>>();

    assert!(wrong.is_empty(), "non-ASCII source bytes: {wrong:?}");
}