1use crate::schema::{Paint, TypeRole};
15use std::rc::Rc;
16
17pub const SPAN_LEN: usize = 36;
21
22pub const SPAN_START_AT: usize = 0;
25pub const SPAN_LENGTH_AT: usize = 4;
26pub const SPAN_ROLE_AT: usize = 8;
27pub const SPAN_FLAGS_AT: usize = 10;
28pub const SPAN_COLOR_AT: usize = 12;
29pub const SPAN_BACKGROUND_AT: usize = 20;
30pub const SPAN_HANDLER_AT: usize = 28;
31
32pub const FLAG_BOLD: u16 = 1;
33pub const FLAG_ITALIC: u16 = 2;
34pub const FLAG_UNDERLINE: u16 = 4;
35pub const FLAG_STRIKETHROUGH: u16 = 8;
36
37#[derive(Clone, Copy, Debug, Default, PartialEq)]
42pub struct TextSpan {
43 pub start: u32,
44 pub length: u32,
45 pub type_role: Option<TypeRole>,
46 pub color: Option<Paint>,
47 pub background: Option<Paint>,
55 pub bold: bool,
56 pub italic: bool,
57 pub underline: bool,
58 pub strikethrough: bool,
59 pub on_click: Option<u64>,
61}
62
63impl TextSpan {
64 pub const fn new(start: u32, length: u32) -> Self {
66 Self {
67 start,
68 length,
69 type_role: None,
70 color: None,
71 background: None,
72 bold: false,
73 italic: false,
74 underline: false,
75 strikethrough: false,
76 on_click: None,
77 }
78 }
79
80 pub const fn bold(mut self) -> Self {
81 self.bold = true;
82 self
83 }
84
85 pub const fn italic(mut self) -> Self {
86 self.italic = true;
87 self
88 }
89
90 pub const fn underline(mut self) -> Self {
91 self.underline = true;
92 self
93 }
94
95 pub const fn strikethrough(mut self) -> Self {
96 self.strikethrough = true;
97 self
98 }
99
100 pub const fn with_role(mut self, role: TypeRole) -> Self {
101 self.type_role = Some(role);
102 self
103 }
104
105 pub const fn with_color(mut self, color: Paint) -> Self {
106 self.color = Some(color);
107 self
108 }
109
110 pub const fn with_background(mut self, background: Paint) -> Self {
113 self.background = Some(background);
114 self
115 }
116
117 fn flags(&self) -> u16 {
118 let mut flags = 0;
119 if self.bold {
120 flags |= FLAG_BOLD;
121 }
122 if self.italic {
123 flags |= FLAG_ITALIC;
124 }
125 if self.underline {
126 flags |= FLAG_UNDERLINE;
127 }
128 if self.strikethrough {
129 flags |= FLAG_STRIKETHROUGH;
130 }
131 flags
132 }
133
134 fn write_into(&self, out: &mut Vec<u8>) {
135 out.extend_from_slice(&self.start.to_le_bytes());
136 out.extend_from_slice(&self.length.to_le_bytes());
137 out.extend_from_slice(&self.type_role.map_or(0, u16::from).to_le_bytes());
138 out.extend_from_slice(&self.flags().to_le_bytes());
139 let paint = self.color.map_or(0, Paint::to_bits);
142 out.extend_from_slice(&(paint as u32).to_le_bytes());
143 out.extend_from_slice(&((paint >> 32) as u32).to_le_bytes());
144 let background = self.background.map_or(0, Paint::to_bits);
145 out.extend_from_slice(&(background as u32).to_le_bytes());
146 out.extend_from_slice(&((background >> 32) as u32).to_le_bytes());
147 let handler = self.on_click.unwrap_or(0);
148 out.extend_from_slice(&(handler as u32).to_le_bytes());
149 out.extend_from_slice(&((handler >> 32) as u32).to_le_bytes());
150 }
151}
152
153type Marked = (&'static [u8], fn(u32, u32) -> TextSpan);
156
157#[derive(Clone, Default, PartialEq)]
162pub struct TextSpans {
163 bytes: Option<Rc<[u8]>>,
164}
165
166impl std::fmt::Debug for TextSpans {
167 fn fmt(&self, formatter: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
168 formatter
169 .debug_struct("TextSpans")
170 .field("spans", &(self.as_bytes().len() / SPAN_LEN))
171 .finish()
172 }
173}
174
175impl TextSpans {
176 pub fn new(spans: impl IntoIterator<Item = TextSpan>) -> Self {
177 let mut bytes = Vec::new();
178 for span in spans {
179 span.write_into(&mut bytes);
180 }
181 Self::from_bytes(bytes)
182 }
183
184 pub fn from_bytes(bytes: impl Into<Rc<[u8]>>) -> Self {
186 let bytes = bytes.into();
187 Self {
188 bytes: (!bytes.is_empty()).then_some(bytes),
189 }
190 }
191
192 pub fn as_bytes(&self) -> &[u8] {
193 self.bytes.as_deref().unwrap_or(&[])
194 }
195
196 pub fn is_empty(&self) -> bool {
197 self.bytes.is_none()
198 }
199
200 pub fn spans(&self) -> impl Iterator<Item = TextSpan> + '_ {
202 self.as_bytes().chunks_exact(SPAN_LEN).map(|record| {
203 let word = |at: usize| {
204 u32::from_le_bytes([record[at], record[at + 1], record[at + 2], record[at + 3]])
205 };
206 let half = |at: usize| u16::from_le_bytes([record[at], record[at + 1]]);
207 let flags = half(SPAN_FLAGS_AT);
208 let paint = u64::from(word(SPAN_COLOR_AT)) | (u64::from(word(SPAN_COLOR_AT + 4)) << 32);
209 let background = u64::from(word(SPAN_BACKGROUND_AT))
210 | (u64::from(word(SPAN_BACKGROUND_AT + 4)) << 32);
211 let handler =
212 u64::from(word(SPAN_HANDLER_AT)) | (u64::from(word(SPAN_HANDLER_AT + 4)) << 32);
213 TextSpan {
214 start: word(SPAN_START_AT),
215 length: word(SPAN_LENGTH_AT),
216 type_role: TypeRole::try_from(half(SPAN_ROLE_AT)).ok(),
217 color: Paint::from_bits(paint),
218 background: Paint::from_bits(background),
219 bold: flags & FLAG_BOLD != 0,
220 italic: flags & FLAG_ITALIC != 0,
221 underline: flags & FLAG_UNDERLINE != 0,
222 strikethrough: flags & FLAG_STRIKETHROUGH != 0,
223 on_click: (handler != 0).then_some(handler),
224 }
225 })
226 }
227
228 pub fn from_markdown(source: &str) -> (String, Self) {
237 let mut text = String::with_capacity(source.len());
238 let mut spans = Vec::new();
239 let bytes = source.as_bytes();
240 let mut at = 0;
241 while at < bytes.len() {
242 let (marker, span_of): Marked = if bytes[at..].starts_with(b"**") {
243 (b"**", |start, length| TextSpan::new(start, length).bold())
244 } else if bytes[at] == b'*' {
245 (b"*", |start, length| TextSpan::new(start, length).italic())
246 } else if bytes[at] == b'`' {
247 (b"`", |start, length| {
248 TextSpan::new(start, length).with_role(TypeRole::Mono)
249 })
250 } else {
251 text.push(source[at..].chars().next().expect("a char boundary"));
252 at += source[at..].chars().next().map_or(1, char::len_utf8);
253 continue;
254 };
255 let body = at + marker.len();
256 let Some(end) = find(&bytes[body..], marker).map(|offset| body + offset) else {
257 text.push_str(&source[at..body]);
259 at = body;
260 continue;
261 };
262 let start = text.len() as u32;
263 text.push_str(&source[body..end]);
264 spans.push(span_of(start, text.len() as u32 - start));
265 at = end + marker.len();
266 }
267 (text, Self::new(spans))
268 }
269}
270
271fn find(haystack: &[u8], needle: &[u8]) -> Option<usize> {
272 haystack
273 .windows(needle.len())
274 .position(|window| window == needle)
275}