1use sequoia_core::Color;
2
3#[derive(Clone, Debug, PartialEq)]
4pub struct SpanAttr {
5 pub bold: bool,
6 pub italic: bool,
7 pub underline: bool,
8 pub strike: bool,
9 pub color: Option<Color>,
10 pub bg: Option<Color>,
11 pub size_delta: f32,
12 pub link: Option<String>,
13 pub code: bool,
14 pub mention: Option<u64>,
15}
16
17impl Default for SpanAttr {
18 fn default() -> Self {
19 Self::plain()
20 }
21}
22
23impl SpanAttr {
24 pub fn plain() -> Self {
25 Self {
26 bold: false,
27 italic: false,
28 underline: false,
29 strike: false,
30 color: None,
31 bg: None,
32 size_delta: 0.0,
33 link: None,
34 code: false,
35 mention: None,
36 }
37 }
38
39 pub fn is_plain(&self) -> bool {
40 !self.bold
41 && !self.italic
42 && !self.underline
43 && !self.strike
44 && self.color.is_none()
45 && self.bg.is_none()
46 && self.size_delta == 0.0
47 && self.link.is_none()
48 && !self.code
49 && self.mention.is_none()
50 }
51
52 pub fn bold() -> Self {
53 Self {
54 bold: true,
55 ..Self::plain()
56 }
57 }
58
59 pub fn link(url: impl Into<String>) -> Self {
60 Self {
61 link: Some(url.into()),
62 ..Self::plain()
63 }
64 }
65}
66
67#[derive(Clone, Debug, PartialEq)]
68pub struct StyledSpan {
69 pub start: usize,
70 pub end: usize,
71 pub attr: SpanAttr,
72}
73
74impl StyledSpan {
75 pub fn new(start: usize, end: usize, attr: SpanAttr) -> Self {
76 Self { start, end, attr }
77 }
78
79 pub fn len(&self) -> usize {
80 self.end.saturating_sub(self.start)
81 }
82
83 pub fn is_empty(&self) -> bool {
84 self.start >= self.end
85 }
86
87 pub fn contains_middle(&self, pos: usize) -> bool {
88 self.start < pos && pos < self.end
89 }
90
91 pub fn overlaps(&self, range: std::ops::Range<usize>) -> bool {
92 self.start < range.end && range.start < self.end
93 }
94
95 pub fn clipped(&self, range: std::ops::Range<usize>) -> Option<Self> {
96 let s = self.start.max(range.start);
97 let e = self.end.min(range.end);
98 if s < e {
99 Some(Self {
100 start: s,
101 end: e,
102 attr: self.attr.clone(),
103 })
104 } else {
105 None
106 }
107 }
108}
109
110pub fn normalize_spans(spans: &mut Vec<StyledSpan>) {
111 spans.retain(|s| s.start < s.end);
112 spans.sort_by_key(|s| (s.start, s.end));
113 let mut out: Vec<StyledSpan> = Vec::with_capacity(spans.len());
114 for mut cur in spans.drain(..) {
115 if cur.start >= cur.end {
116 continue;
117 }
118 if let Some(prev) = out.last_mut() {
119 if cur.start < prev.end {
120 if prev.attr == cur.attr {
121 prev.end = prev.end.max(cur.end);
122 continue;
123 }
124 cur.start = prev.end;
125 if cur.start >= cur.end {
126 continue;
127 }
128 }
129 if prev.end == cur.start && prev.attr == cur.attr {
130 prev.end = cur.end;
131 continue;
132 }
133 }
134 out.push(cur);
135 }
136 *spans = out;
137}
138
139pub fn hash_span_attr<H: std::hash::Hasher>(a: &SpanAttr, h: &mut H) {
140 use std::hash::Hash;
141 a.bold.hash(h);
142 a.italic.hash(h);
143 a.underline.hash(h);
144 a.strike.hash(h);
145 a.color
146 .map(|c| (c.r.to_bits(), c.g.to_bits(), c.b.to_bits(), c.a.to_bits()))
147 .hash(h);
148 a.bg.map(|c| (c.r.to_bits(), c.g.to_bits(), c.b.to_bits(), c.a.to_bits()))
149 .hash(h);
150 let q = if a.size_delta.is_finite() {
151 (a.size_delta * 4.0).round() as i32
152 } else {
153 0
154 };
155 q.hash(h);
156 a.link.hash(h);
157 a.code.hash(h);
158 a.mention.hash(h);
159}
160
161pub fn hash_spans_rebased<H: std::hash::Hasher>(spans: &[StyledSpan], base: usize, h: &mut H) {
162 use std::hash::Hash;
163 spans.len().hash(h);
164 for s in spans {
165 if s.is_empty() {
166 continue;
167 }
168 s.start.saturating_sub(base).hash(h);
169 s.end.saturating_sub(base).hash(h);
170 hash_span_attr(&s.attr, h);
171 }
172}
173
174pub fn spans_hash(spans: &[StyledSpan]) -> u64 {
175 use std::collections::hash_map::DefaultHasher;
176 use std::hash::Hasher;
177 let mut h = DefaultHasher::new();
178 hash_spans_rebased(spans, 0, &mut h);
179 h.finish()
180}
181
182pub fn line_hash(text: &str, spans: &[StyledSpan], width: f32) -> u64 {
183 use std::collections::hash_map::DefaultHasher;
184 use std::hash::{Hash, Hasher};
185 let mut h = DefaultHasher::new();
186 text.hash(&mut h);
187 hash_spans_rebased(spans, 0, &mut h);
188 ((width.max(0.0) * 4.0).round() as u32).hash(&mut h);
189 h.finish()
190}
191
192pub fn rebase_spans(spans: &[StyledSpan], base: usize) -> Vec<StyledSpan> {
193 spans
194 .iter()
195 .map(|s| {
196 StyledSpan::new(
197 s.start.saturating_sub(base),
198 s.end.saturating_sub(base),
199 s.attr.clone(),
200 )
201 })
202 .collect()
203}
204
205#[cfg(test)]
206mod tests {
207 use super::*;
208
209 #[test]
210 fn plain_roundtrip() {
211 let p = SpanAttr::plain();
212 assert!(p.is_plain());
213 assert_eq!(p, SpanAttr::default());
214 let b = SpanAttr::bold();
215 assert!(!b.is_plain());
216 assert!(b.bold);
217 }
218
219 #[test]
220 fn neighbours_merge() {
221 let mut v = vec![
222 StyledSpan::new(0, 2, SpanAttr::bold()),
223 StyledSpan::new(2, 5, SpanAttr::bold()),
224 StyledSpan::new(5, 6, SpanAttr::plain()),
225 ];
226
227 normalize_spans(&mut v);
228 assert_eq!(v.len(), 2);
229 assert_eq!((v[0].start, v[0].end), (0, 5));
230 }
231
232 #[test]
233 fn overlap_truncates_left_biased() {
234 let mut v = vec![
235 StyledSpan::new(0, 5, SpanAttr::bold()),
236 StyledSpan::new(3, 8, SpanAttr::link("x")),
237 ];
238 normalize_spans(&mut v);
239 assert_eq!(v.len(), 2);
240 assert_eq!((v[0].start, v[0].end), (0, 5));
241 assert_eq!((v[1].start, v[1].end), (5, 8));
242 }
243
244 #[test]
245 fn empties_dropped_and_sorted() {
246 let mut v = vec![
247 StyledSpan::new(8, 9, SpanAttr::bold()),
248 StyledSpan::new(4, 4, SpanAttr::bold()),
249 StyledSpan::new(0, 2, SpanAttr::bold()),
250 ];
251 normalize_spans(&mut v);
252 assert_eq!(v.len(), 2);
253 assert_eq!((v[0].start, v[0].end), (0, 2));
254 assert_eq!((v[1].start, v[1].end), (8, 9));
255 }
256
257 #[test]
258 fn line_hash_contract() {
259 let a = vec![StyledSpan::new(0, 2, SpanAttr::bold())];
260 let h1 = line_hash("abcdef", &a, 100.0);
261
262 assert_eq!(h1, line_hash("abcdef", &a, 100.0));
263
264 assert_ne!(h1, line_hash("abcdeg", &a, 100.0));
265 assert_ne!(
266 h1,
267 line_hash(
268 "abcdef",
269 &[StyledSpan::new(0, 2, SpanAttr::link("x"))],
270 100.0
271 )
272 );
273 assert_ne!(
274 h1,
275 line_hash("abcdef", &[StyledSpan::new(1, 3, SpanAttr::bold())], 100.0)
276 );
277 assert_ne!(h1, line_hash("abcdef", &[], 100.0));
278 assert_ne!(h1, line_hash("abcdef", &a, 200.0));
279
280 let abs_a = vec![StyledSpan::new(100, 102, SpanAttr::bold())];
281 let abs_b = vec![StyledSpan::new(200, 202, SpanAttr::bold())];
282 assert_eq!(
283 line_hash("abcdef", &rebase_spans(&abs_a, 100), 0.0),
284 line_hash("abcdef", &rebase_spans(&abs_b, 200), 0.0)
285 );
286 }
287}