use unicode_segmentation::UnicodeSegmentation;
use unicode_width::UnicodeWidthStr;
pub fn printable_grapheme(grapheme: &str) -> &str {
if grapheme.chars().any(char::is_control) {
"\u{fffd}"
} else {
grapheme
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct GraphemeSpan {
pub byte: usize,
pub cell: u16,
pub width: u8,
}
#[derive(Debug, Clone, Default)]
pub struct LineLayout {
spans: Vec<GraphemeSpan>,
pub len_bytes: usize,
pub width: u16,
}
impl LineLayout {
pub fn build(text: &str, tab: u16) -> Self {
let tab = tab.max(1);
let mut spans = Vec::with_capacity(text.len() / 2 + 4);
let mut cell: u16 = 0;
for (byte, g) in text.grapheme_indices(true) {
let w = if g == "\t" {
(tab - cell % tab) as u8
} else {
UnicodeWidthStr::width(printable_grapheme(g)).min(u8::MAX as usize) as u8
};
spans.push(GraphemeSpan {
byte,
cell,
width: w,
});
cell = cell.saturating_add(w as u16);
}
LineLayout {
spans,
len_bytes: text.len(),
width: cell,
}
}
pub fn spans(&self) -> &[GraphemeSpan] {
&self.spans
}
pub fn cell_at_byte(&self, byte: usize) -> u16 {
if byte >= self.len_bytes {
return self.width;
}
self.spans
.iter()
.rev()
.find(|s| s.byte <= byte)
.map(|s| s.cell)
.unwrap_or(0)
}
pub fn byte_at_cell(&self, cell: u16) -> usize {
match self.spans.iter().rev().find(|s| s.cell <= cell) {
Some(s) => s.byte,
None => 0,
}
}
pub fn is_ascii_fast(&self) -> bool {
self.spans.iter().all(|s| s.width == 1)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn ascii_is_identity() {
let l = LineLayout::build("hello", 8);
assert_eq!(l.width, 5);
assert_eq!(l.cell_at_byte(3), 3);
assert_eq!(l.byte_at_cell(3), 3);
assert!(l.is_ascii_fast());
}
#[test]
fn cjk_is_two_cells() {
let l = LineLayout::build("a界b", 8);
assert_eq!(l.width, 4);
assert_eq!(l.cell_at_byte(1), 1); assert_eq!(l.cell_at_byte(4), 3); assert_eq!(l.byte_at_cell(3), 4);
}
#[test]
fn emoji_cluster_is_one_unit() {
let l = LineLayout::build("x\u{1F9D1}\u{200D}\u{1F680}y", 8); assert_eq!(l.spans().len(), 3);
assert_eq!(l.spans()[1].width, 2);
assert_eq!(l.width, 4);
}
#[test]
fn tab_expands_to_its_stop() {
let l = LineLayout::build("ab\tc", 4);
assert_eq!(l.spans()[2].width, 2); assert_eq!(l.cell_at_byte(3), 4); assert_eq!(l.width, 5);
}
}