use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
const ZERO_WIDTH_JOINER: &str = "\u{200d}";
const VARIATION_SELECTOR_16: &str = "\u{fe0f}";
const SKIN_TONES: [&str; 5] = [
"\u{1f3fb}", "\u{1f3fc}", "\u{1f3fd}", "\u{1f3fe}", "\u{1f3ff}", ];
lazy_static! {
static ref OTHER_PUNCTUATION: Vec<char> = vec!['…', '⋯',];
}
pub fn is_punctuation(character: char) -> bool {
character.is_ascii_punctuation() || OTHER_PUNCTUATION.contains(&character)
}
pub fn display_width(string: &str) -> usize {
let unicode_chars = string.graphemes(true);
let mut width = 0;
for c in unicode_chars {
width += display_width_char(c);
}
width
}
fn display_width_char(string: &str) -> usize {
if string == ZERO_WIDTH_JOINER || string == VARIATION_SELECTOR_16 {
return 0;
}
if string.contains(ZERO_WIDTH_JOINER) {
return 2;
}
for skin_tone in SKIN_TONES {
if string.contains(skin_tone) {
return 2;
}
}
match string {
"\t" => {
4
}
_ => UnicodeWidthStr::width(string),
}
}
#[derive(Debug, PartialEq)]
pub struct MarkerStats {
pub bytes_index: usize, pub char_count: usize, }
pub fn line_length_stats(line: &str, max_width: usize) -> (usize, MarkerStats) {
let unicode_chars = line.graphemes(true);
let mut bytes_index = 0;
let mut char_count = 0;
let mut width = 0;
for c in unicode_chars {
width += display_width_char(c);
if width <= max_width {
char_count += 1;
bytes_index += c.len();
}
}
(
width,
MarkerStats {
bytes_index,
char_count,
},
)
}
pub fn character_count_for_bytes_index(string: &str, bytes_index: usize) -> usize {
match &string.get(0..bytes_index) {
Some(sub_string) => {
sub_string.graphemes(true).count() + 1
}
None => {
error!(
"character_count_for_bytes_index: Unable to determine substring length.\n\
Please report this error: https://github.com/tombruijn/lintje/issues\n\
String: {:?}\nbytes_index: {:?}",
string, bytes_index
);
bytes_index
}
}
}
pub fn pluralize(label: &str, count: usize) -> String {
let plural = if count == 1 { "" } else { "s" };
format!("{}{}", label, plural)
}
#[cfg(test)]
mod tests {
use super::{character_count_for_bytes_index, display_width, line_length_stats, MarkerStats};
#[test]
fn test_character_index_for_bytes() {
let s = "Hello world!";
assert_eq!(character_count_for_bytes_index(s, s.len()), 13);
let s = "Héllo world!";
assert_eq!(character_count_for_bytes_index(s, s.len()), 13);
let s = "Hellあ world!";
assert_eq!(character_count_for_bytes_index(s, s.len()), 13);
let s = "Hellö̲ world!";
assert_eq!(character_count_for_bytes_index(s, s.len()), 13);
let s = "Hello world!";
assert_eq!(character_count_for_bytes_index(s, s.len()), 13);
let s = "Hell😀 world!";
assert_eq!(character_count_for_bytes_index(s, s.len()), 13);
let s = "Hell👩🔬 world!";
assert_eq!(character_count_for_bytes_index(s, s.len()), 13);
let s = "Hello world!"; assert_eq!(character_count_for_bytes_index(s, 6), 7);
let s = "Hello 😀 world!"; assert_eq!(character_count_for_bytes_index(s, 11), 9);
let s = "Hello 👩🔬 world!"; assert_eq!(character_count_for_bytes_index(s, 18), 9);
}
fn assert_width(string: &str, width: usize) {
assert_eq!(
display_width(string),
width,
"String `{}` is not {} width",
string,
width
);
}
#[test]
fn test_display_width() {
assert_width("a", 1);
assert_width("\t", 4);
assert_width("…", 1);
assert_width("é", 1);
assert_width("ö", 1);
assert_width("ø", 1);
assert_width("a̐", 1);
assert_width("é", 1);
assert_width("ö̲", 1);
assert_width("ぁ", 2);
assert_width("あ", 2);
assert_width("\u{200d}", 0);
assert_width("\u{fe0f}", 0);
assert_width("0️⃣", 1);
assert_width("1️⃣", 1);
assert_width("#️⃣", 1);
assert_width("﹟", 2);
assert_width("#", 2);
assert_width("*️⃣", 1);
assert_width("*", 2);
assert_width("❗️", 2);
assert_width("☁️", 1);
assert_width("❤️", 1);
assert_width("☂️", 1);
assert_width("✏️", 1);
assert_width("✂️", 1);
assert_width("☎️", 1);
assert_width("✈️", 1);
assert_width("👁", 1); assert_width("👁️", 1); assert_width("👁️🗨️", 2);
assert_width("🚀", 2);
assert_width("👩", 2);
assert_width("👩🏻", 2);
assert_width("👩🏼", 2);
assert_width("👩🏽", 2);
assert_width("👩🏾", 2);
assert_width("👩🏿", 2);
assert_width("👩🔬", 2);
assert_width("🧘🏽♀️", 2);
assert_width("👨🏻❤️👨🏿", 2);
assert_width("🧑🦲", 2);
assert_width("👨🏿🦲", 2);
assert_width("abc", 3);
assert_width(&"a".repeat(50), 50);
assert_width("!*_-=+|[]`'.,<>():;!@#$%^&{}10/", 31);
assert_width("I am a string with multiple 😁🚀あ", 34);
assert_width("👩🔬👩🔬", 4);
}
#[test]
fn test_line_stats() {
assert_eq!(
line_length_stats("Lorem ipsum", 6),
(
11,
MarkerStats {
bytes_index: 6,
char_count: 6,
}
)
);
}
#[test]
fn test_line_stats_unicode() {
let (width, line_stats) = line_length_stats("Aaa̐Bb", 3);
assert_eq!("a̐".chars().count(), 2);
assert_eq!(width, 5);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 5,
char_count: 3,
}
);
let (width, line_stats) = line_length_stats("AaあBb", 4);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 5,
char_count: 3,
}
);
}
#[test]
fn test_line_stats_emoji() {
let (width, line_stats) = line_length_stats("Aa😀Bb", 2);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 2,
char_count: 2,
}
);
let (width, line_stats) = line_length_stats("Aa😀Bb", 3);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 2,
char_count: 2,
}
);
let (width, line_stats) = line_length_stats("Aa😀Bb", 4);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 6,
char_count: 3,
}
);
}
#[test]
fn test_line_stats_multi_char_emoji() {
let (width, line_stats) = line_length_stats("Aa👩🔬Bb", 2);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 2,
char_count: 2,
}
);
let (width, line_stats) = line_length_stats("Aa👩🔬Bb", 3);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 2,
char_count: 2,
}
);
let (width, line_stats) = line_length_stats("Aa👩🔬Bb", 4);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 13,
char_count: 3,
}
);
let (width, line_stats) = line_length_stats("Aa👩🔬Bb", 5);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 14,
char_count: 4,
}
);
let (width, line_stats) = line_length_stats("Aa👩🔬Bb", 6);
assert_eq!(width, 6);
assert_eq!(
line_stats,
MarkerStats {
bytes_index: 15,
char_count: 5,
}
);
}
}