1use std::fmt;
2
3use super::PrintableText;
4
5pub(crate) static DEFAULT_TAB_POLICY: TabPolicy = TabPolicy::spaces(4);
6
7#[derive(Debug, Clone, PartialEq, Eq)]
14pub struct TabPolicy {
15 width: u16,
16 marker: Option<String>,
17}
18
19impl Default for TabPolicy {
20 fn default() -> Self {
21 Self::spaces(4)
22 }
23}
24
25impl TabPolicy {
26 pub const fn spaces(width: u16) -> Self {
28 Self {
29 width,
30 marker: None,
31 }
32 }
33
34 pub fn with_marker(width: u16, marker: impl Into<String>) -> Result<Self, InvalidTabMarker> {
40 let marker = marker.into();
41 if marker.is_empty() {
42 return Ok(Self::spaces(width));
43 }
44 if marker.chars().any(char::is_control) {
45 return Err(InvalidTabMarker::ControlCharacter);
46 }
47 let printable = PrintableText::new(&marker);
48 if printable.graphemes().any(|grapheme| grapheme.width() == 0) {
49 return Err(InvalidTabMarker::ZeroWidthGrapheme);
50 }
51 let marker_width = printable.width();
52 if marker_width > usize::from(width) {
53 return Err(InvalidTabMarker::TooWide {
54 width,
55 marker_width,
56 });
57 }
58 Ok(Self {
59 width,
60 marker: Some(marker),
61 })
62 }
63
64 pub const fn width(&self) -> u16 {
66 self.width
67 }
68
69 pub fn marker(&self) -> Option<&str> {
71 self.marker.as_deref()
72 }
73}
74
75#[derive(Debug, Clone, Copy, PartialEq, Eq)]
77pub enum InvalidTabMarker {
78 ControlCharacter,
80 ZeroWidthGrapheme,
82 TooWide { width: u16, marker_width: usize },
84}
85
86impl fmt::Display for InvalidTabMarker {
87 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
88 match self {
89 Self::ControlCharacter => {
90 formatter.write_str("a tab marker must not contain control characters")
91 }
92 Self::ZeroWidthGrapheme => {
93 formatter.write_str("every tab marker grapheme must occupy at least one cell")
94 }
95 Self::TooWide {
96 width,
97 marker_width,
98 } => write!(
99 formatter,
100 "tab marker occupies {marker_width} cells, exceeding its width of {width}"
101 ),
102 }
103 }
104}
105
106impl std::error::Error for InvalidTabMarker {}
107
108#[cfg(test)]
109mod tests {
110 use super::*;
111
112 #[test]
113 fn empty_marker_is_canonicalized_to_spaces() {
114 assert_eq!(TabPolicy::with_marker(3, "").unwrap(), TabPolicy::spaces(3));
115 }
116
117 #[test]
118 fn marker_must_be_printable_and_fit_its_width() {
119 assert_eq!(
120 TabPolicy::with_marker(4, "→\t"),
121 Err(InvalidTabMarker::ControlCharacter)
122 );
123 assert_eq!(
124 TabPolicy::with_marker(1, "日本"),
125 Err(InvalidTabMarker::TooWide {
126 width: 1,
127 marker_width: 4,
128 })
129 );
130 assert_eq!(
131 TabPolicy::with_marker(1, "\u{301}"),
132 Err(InvalidTabMarker::ZeroWidthGrapheme)
133 );
134 assert_eq!(
135 TabPolicy::with_marker(1, "\u{301}x"),
136 Err(InvalidTabMarker::ZeroWidthGrapheme)
137 );
138 }
139}